From aca58a3c7e18ee5c3784da0826ae21553811eea9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:19:01 +0200 Subject: [PATCH 001/926] Extend internal API to be able to emit multiple errors --- .../lang/ast/CompilingClassloader.class.php | 2 +- src/main/php/lang/ast/Errors.class.php | 38 +++++++++++++++++++ src/main/php/lang/ast/Parse.class.php | 21 +++++++++- .../php/xp/compiler/CompileRunner.class.php | 9 +++-- .../ast/unittest/parse/ErrorsTest.class.php | 6 +-- .../ast/unittest/parse/TypesTest.class.php | 8 ++-- 6 files changed, 70 insertions(+), 14 deletions(-) create mode 100755 src/main/php/lang/ast/Errors.class.php diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 2bd6d066..9122f587 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -170,7 +170,7 @@ public function loadClassBytes($class) { $emitter->emit($parse->execute()); return $declaration->getBytes(); - } catch (Error $e) { + } catch (Errors $e) { $message= sprintf('Syntax error in %s, line %d: %s', $e->getFile(), $e->getLine(), $e->getMessage()); throw new ClassFormatException($message); } finally { diff --git a/src/main/php/lang/ast/Errors.class.php b/src/main/php/lang/ast/Errors.class.php new file mode 100755 index 00000000..16fa5003 --- /dev/null +++ b/src/main/php/lang/ast/Errors.class.php @@ -0,0 +1,38 @@ +getMessage(); + break; + + default: + $message= "Errors {\n"; + foreach ($errors as $error) { + $message.= ' '.$error->message.' at line '.$error->line."\n"; + } + $message.= '}'; + } + parent::__construct($message); + $this->file= $file; + $this->line= $errors[0]->getLine(); + } + + public function diagnostics($indent= '') { + return str_replace("\n", "\n".$indent, $this->message); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 487f89d3..d3cbacea 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -43,6 +43,7 @@ class Parse { private $tokens, $file, $token, $scope; private $comment= null; private $queue= []; + private $errors= []; static function __static() { self::symbol(':'); @@ -1423,7 +1424,7 @@ private function suffix($id, $bp, $led= null) { */ private function raise($message, $context= null) { $context && $message.= ' in '.$context; - throw new Error($message, $this->file, $this->token->line); + $this->errors[]= new Error($message, $this->file, $this->token->line); } /** @@ -1476,8 +1477,24 @@ private function advance() { return $node; } + /** + * Parses given file, returning AST nodes. + * + * @return iterable + * @throws lang.ast.Errors + */ public function execute() { $this->token= $this->advance(); - return $this->top(); + try { + foreach ($this->top() as $node) { + yield $node; + } + } catch (Error $e) { + $this->errors[]= $e; + } + + if ($this->errors) { + throw new Errors($this->errors, $this->file); + } } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 82b0ce00..2f3a63fb 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -76,9 +76,10 @@ public static function main(array $args) { $total= $errors= 0; $time= 0.0; foreach ($input as $path => $in) { + $file= $path->toString('/'); $t->start(); try { - $parse= new Parse(new Tokens(new StreamTokenizer($in))); + $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); foreach (Transformations::registered() as $kind => $function) { $emitter->transform($kind, $function); @@ -86,10 +87,10 @@ public static function main(array $args) { $emitter->emit($parse->execute()); $t->stop(); - Console::$err->writeLinef('> %s (%.3f seconds)', $path->toString('/'), $t->elapsedTime()); - } catch (Error $e) { + Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); + } catch (Errors $e) { $t->stop(); - Console::$err->writeLinef('! %s %s at line %d', $path->toString('/'), $e->getMessage(), $e->getLine()); + Console::$err->writeLinef('! %s: %s ', $file, $e->diagnostics(' ')); $errors++; } finally { $total++; diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index a11fded9..d258e2d0 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -1,6 +1,6 @@ fail('No exception raised', null, Error::class); - } catch (Error $expected) { + $this->fail('No exception raised', null, Errors::class); + } catch (Errors $expected) { $this->assertEquals($message, $expected->getMessage()); } } diff --git a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php index 647a1a2a..ed7dacee 100755 --- a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php @@ -1,6 +1,6 @@ parse('class A { public function b() { } public function b() { }}')); } - #[@test, @expect(class= Error::class, withMessage= 'Cannot redeclare property $b')] + #[@test, @expect(class= Errors::class, withMessage= 'Cannot redeclare property $b')] public function cannot_redeclare_property() { iterator_to_array($this->parse('class A { public $b; private $b; }')); } - #[@test, @expect(class= Error::class, withMessage= 'Cannot redeclare constant B')] + #[@test, @expect(class= Errors::class, withMessage= 'Cannot redeclare constant B')] public function cannot_redeclare_constant() { iterator_to_array($this->parse('class A { const B = 1; const B = 3; }')); } From db16067ebd7c8a6c6a10c520a1149ff1d9449279 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:29:54 +0200 Subject: [PATCH 002/926] Handle missing semicolons gracefully, do not abort parsing --- src/main/php/lang/ast/Parse.class.php | 9 ++++++++- .../loader/CompilingClassLoaderTest.class.php | 12 ++++++------ .../php/lang/ast/unittest/parse/ErrorsTest.class.php | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index d3cbacea..d9a79599 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1328,7 +1328,14 @@ private function statement() { } $expr= $this->expression(0); - $this->token= $this->expect(';'); + + if (';' !== $this->token->symbol->id) { + $message= sprintf('Missing semicolon before %s %s', $this->token->symbol->id, $this->token->value); + $this->errors[]= new Error($message, $this->file, $this->token->line); + } else { + $this->token= $this->advance(); + } + return $expr; } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 0d2b1ce7..a7bf85ea 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -1,12 +1,12 @@ "); + FileUtil::setContents($f, "getPath()); $loader= new CompilingClassLoader(self::$runtime); @@ -37,7 +37,7 @@ public function load_class_with_syntax_errors() { $loader->loadClass('Errors'); $this->fail('No exception raised', null, ClassFormatException::class); } catch (ClassFormatException $expected) { - $this->assertEquals('Syntax error in Errors.php, line 2: Expected ";", have "Syntax"', $expected->getMessage()); + $this->assertEquals('Syntax error in Errors.php, line 1: Expected "{", have "(end)"', $expected->getMessage()); } finally { ClassLoader::removeLoader($cl); $f->unlink(); diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index d258e2d0..1acd2ae0 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -23,7 +23,7 @@ private function assertError($message, $parse) { #[@test] public function missing_semicolon() { $this->assertError( - 'Expected ";", have "(variable)"', + 'Missing semicolon before (variable) b', $this->parse('$a= 1 $b= 1;') ); } From cdd311bc761c9155800fa46d4e662aced184e033 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:34:32 +0200 Subject: [PATCH 003/926] Use raise() --- src/main/php/lang/ast/Parse.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index d9a79599..7149510a 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1330,8 +1330,7 @@ private function statement() { $expr= $this->expression(0); if (';' !== $this->token->symbol->id) { - $message= sprintf('Missing semicolon before %s %s', $this->token->symbol->id, $this->token->value); - $this->errors[]= new Error($message, $this->file, $this->token->line); + $this->raise(sprintf('Missing semicolon before %s', $this->token->symbol->id)); } else { $this->token= $this->advance(); } From 53db3bfe3823c73875912593f2f5e49ce3d622b7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:35:30 +0200 Subject: [PATCH 004/926] Fix imports --- src/main/php/xp/compiler/CompileRunner.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 2f3a63fb..b3080c14 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -3,7 +3,7 @@ use io\Path; use lang\Runtime; use lang\ast\Emitter; -use lang\ast\Error; +use lang\ast\Errors; use lang\ast\Parse; use lang\ast\Tokens; use lang\ast\transform\Transformations; From 1e051686fc78fa0588b439f8667e600ae117909c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:36:55 +0200 Subject: [PATCH 005/926] Fix tests --- src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 1acd2ae0..831b111e 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -23,7 +23,7 @@ private function assertError($message, $parse) { #[@test] public function missing_semicolon() { $this->assertError( - 'Missing semicolon before (variable) b', + 'Missing semicolon before (variable)', $this->parse('$a= 1 $b= 1;') ); } From a34b97e0648e6ca78aaecccfcc94c8c2dcfbb1e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:47:59 +0200 Subject: [PATCH 006/926] Handle unexpected tokens inside types gracefully & continue parsing --- src/main/php/lang/ast/Parse.class.php | 9 +++++++-- .../php/lang/ast/unittest/parse/ErrorsTest.class.php | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 7149510a..f9d93b48 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1280,7 +1280,12 @@ private function body() { } else if ($type= $this->type()) { continue; } else { - $this->expect('a type, modifier, property, annotation, method or }', 'type body'); + $this->raise(sprintf( + 'Expected a type, modifier, property, annotation, method or "}", have "%s"', + $this->token->symbol->id + )); + $this->token= $this->advance(); + if (null === $this->token->value) break; } } return $body; @@ -1330,7 +1335,7 @@ private function statement() { $expr= $this->expression(0); if (';' !== $this->token->symbol->id) { - $this->raise(sprintf('Missing semicolon before %s', $this->token->symbol->id)); + $this->raise('Missing semicolon before '.$this->token->symbol->id); } else { $this->token= $this->advance(); } diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 831b111e..7500be8d 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -47,8 +47,8 @@ public function unclosed_brace_in_parameters() { #[@test] public function unclosed_type() { $this->assertError( - 'Expected "a type, modifier, property, annotation, method or }", have "-" in type body', - $this->parse('class T { -') + 'Expected a type, modifier, property, annotation, method or "}", have "-"', + $this->parse('class T { - }') ); } From e3a55a9bd879b69d671a515b59a6d31349c994fa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Jun 2018 00:54:16 +0200 Subject: [PATCH 007/926] Use "after X statement" instead of "before Y token" for missing semicolons --- src/main/php/lang/ast/Parse.class.php | 7 ++++--- src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index f9d93b48..d5398f70 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1335,7 +1335,7 @@ private function statement() { $expr= $this->expression(0); if (';' !== $this->token->symbol->id) { - $this->raise('Missing semicolon before '.$this->token->symbol->id); + $this->raise('Missing semicolon after '.$expr->kind.' statement', null, $expr->line); } else { $this->token= $this->advance(); } @@ -1431,11 +1431,12 @@ private function suffix($id, $bp, $led= null) { * * @param string $error * @param string $context + * @param int $line * @return void */ - private function raise($message, $context= null) { + private function raise($message, $context= null, $line= null) { $context && $message.= ' in '.$context; - $this->errors[]= new Error($message, $this->file, $this->token->line); + $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); } /** diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 7500be8d..376c2a0d 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -23,7 +23,7 @@ private function assertError($message, $parse) { #[@test] public function missing_semicolon() { $this->assertError( - 'Missing semicolon before (variable)', + 'Missing semicolon after assignment statement', $this->parse('$a= 1 $b= 1;') ); } From 186b5a660e94be568016c4b0eda0bdb9f2383046 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 20:44:02 +0200 Subject: [PATCH 008/926] Fix line number of end token --- src/main/php/lang/ast/Parse.class.php | 3 ++- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 965baeea..dfaacbce 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1521,9 +1521,10 @@ private function expect($id, $context= null) { } private function advance() { + static $line= 1; + if ($this->queue) return array_shift($this->queue); - $line= 1; while ($this->tokens->valid()) { $type= $this->tokens->key(); list($value, $line)= $this->tokens->current(); diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 8c6f82e6..4ef35bbd 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -48,7 +48,7 @@ public function load_class() { #[@test, @expect( # class= ClassFormatException::class, - # withMessage= 'Syntax error in Errors.php, line 1: Expected "{", have "(end)"' + # withMessage= 'Syntax error in Errors.php, line 2: Expected "{", have "(end)"' #)] public function load_class_with_syntax_errors() { $this->load('Errors', " Date: Sun, 24 Jun 2018 21:40:44 +0200 Subject: [PATCH 009/926] Add anticipate(), which is like expect(), but does not terminate WIP --- src/main/php/lang/ast/Parse.class.php | 35 +++++++++++++++---- .../ast/unittest/parse/ErrorsTest.class.php | 8 +++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index dfaacbce..c07b47fd 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -123,9 +123,9 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('->', 80, function($node, $left) { if ('{' === $this->token->value) { - $this->token= $this->expect('{'); + $this->token= $this->advance(); $expr= $this->expression(0); - $this->token= $this->expect('}'); + $this->token= $this->anticipate('}'); } else { $expr= $this->token; $this->token= $this->advance(); @@ -138,9 +138,9 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('?->', 80, function($node, $left) { if ('{' === $this->token->value) { - $this->token= $this->expect('{'); + $this->token= $this->advance(); $expr= $this->expression(0); - $this->token= $this->expect('}'); + $this->token= $this->anticipate('}'); } else { $expr= $this->token; $this->token= $this->advance(); @@ -162,9 +162,9 @@ public function __construct($tokens, $file= null, $scope= null) { $signature= new Signature([new Parameter($left->value, null)], null); if ('{' === $this->token->value) { - $this->token= $this->expect('{'); + $this->token= $this->advance(); $statements= $this->statements(); - $this->token= $this->expect('}'); + $this->token= $this->anticipate('}'); } else { $statements= $this->expressionWithThrows(0); } @@ -185,10 +185,11 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('[', 80, function($node, $left) { if (']' === $this->token->value) { $expr= null; + $this->token= $this->advance(); } else { $expr= $this->expression(0); + $this->token= $this->anticipate(']'); } - $this->token= $this->expect(']'); $node->value= new OffsetExpression($left, $expr); $node->kind= 'offset'; @@ -1499,6 +1500,26 @@ private function raise($message, $context= null, $line= null) { $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); } + /** + * Expect a given token, raise an error if another is encountered + * + * @param string $id + * @param string $context + * @return var + */ + private function anticipate($id, $context= null) { + if ($id === $this->token->symbol->id) return $this->advance(); + + $message= sprintf( + 'Expected "%s", have "%s"%s', + $id, + $this->token->value ?: $this->token->symbol->id, + $context ? ' in '.$context : '' + ); + $this->errors[]= new Error($message, $this->file, $this->token->line); + return $this->token; + } + /** * Expect a given token, raise an error if another is encountered * diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 376c2a0d..02f1c913 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -75,4 +75,12 @@ public function unclosed_annotation() { $this->parse('<assertError( + 'Expected "]", have ";"', + $this->parse('$a[$s[0]= 5;') + ); + } } \ No newline at end of file From 1bc867b903433152ef28aec1182c05ca3523f536 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 21:42:53 +0200 Subject: [PATCH 010/926] Migrate more cases to keep parsing on errors --- src/main/php/lang/ast/Parse.class.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index c07b47fd..c9dfdf84 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -125,7 +125,7 @@ public function __construct($tokens, $file= null, $scope= null) { if ('{' === $this->token->value) { $this->token= $this->advance(); $expr= $this->expression(0); - $this->token= $this->anticipate('}'); + $this->token= $this->next('}'); } else { $expr= $this->token; $this->token= $this->advance(); @@ -140,7 +140,7 @@ public function __construct($tokens, $file= null, $scope= null) { if ('{' === $this->token->value) { $this->token= $this->advance(); $expr= $this->expression(0); - $this->token= $this->anticipate('}'); + $this->token= $this->next('}'); } else { $expr= $this->token; $this->token= $this->advance(); @@ -164,7 +164,7 @@ public function __construct($tokens, $file= null, $scope= null) { if ('{' === $this->token->value) { $this->token= $this->advance(); $statements= $this->statements(); - $this->token= $this->anticipate('}'); + $this->token= $this->next('}'); } else { $statements= $this->expressionWithThrows(0); } @@ -176,7 +176,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('(', 80, function($node, $left) { $arguments= $this->arguments(); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); $node->value= new InvokeExpression($left, $arguments); $node->kind= 'invoke'; return $node; @@ -188,7 +188,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); } else { $expr= $this->expression(0); - $this->token= $this->anticipate(']'); + $this->token= $this->next(']'); } $node->value= new OffsetExpression($left, $expr); @@ -198,7 +198,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('{', 80, function($node, $left) { $expr= $this->expression(0); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); $node->value= new OffsetExpression($left, $expr); $node->kind= 'offset'; @@ -207,7 +207,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->infix('?', 80, function($node, $left) { $when= $this->expressionWithThrows(0); - $this->token= $this->expect(':'); + $this->token= $this->next(':'); $else= $this->expressionWithThrows(0); $node->value= new TernaryExpression($left, $when, $else); $node->kind= 'ternary'; @@ -1507,7 +1507,7 @@ private function raise($message, $context= null, $line= null) { * @param string $context * @return var */ - private function anticipate($id, $context= null) { + private function next($id, $context= null) { if ($id === $this->token->symbol->id) return $this->advance(); $message= sprintf( From 6380d9bf93f6e111658ec8a93d3a5dfe031a7470 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 21:45:25 +0200 Subject: [PATCH 011/926] Migrate trailing semicolons && expect() -> next() --- src/main/php/lang/ast/Parse.class.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index c9dfdf84..84b382db 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -280,9 +280,9 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); if ('{' === $this->token->value) { - $this->token= $this->expect('{'); + $this->token= $this->advance(); $statements= $this->statements(); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); } else { $statements= $this->expressionWithThrows(0); } @@ -454,7 +454,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); if ('=' === $this->token->value) { - $this->token= $this->expect('='); + $this->token= $this->advance(); $initial= $this->expression(0); } else { $initial= null; @@ -559,7 +559,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->scope->import($import); } - $this->token= $this->expect(';'); + $this->token= $this->next(';'); $node->value= $types; return $node; }); @@ -617,7 +617,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); } else { $node->value= $this->expression(0); - $this->token= $this->expect(';'); + $this->token= $this->next(';'); } $node->kind= 'break'; @@ -630,7 +630,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); } else { $node->value= $this->expression(0); - $this->token= $this->expect(';'); + $this->token= $this->next(';'); } $node->kind= 'continue'; @@ -644,7 +644,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->expect('('); $expression= $this->expression(0); $this->token= $this->expect(')'); - $this->token= $this->expect(';'); + $this->token= $this->next(';'); $node->value= new DoLoop($expression, $block); $node->kind= 'do'; @@ -705,7 +705,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->stmt('throw', function($node) { $node->value= $this->expression(0); $node->kind= 'throw'; - $this->token= $this->expect(';'); + $this->token= $this->next(';'); return $node; }); From 5ed1994db826b614463629a35645098d5a313f9a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 21:54:15 +0200 Subject: [PATCH 012/926] Continue parsing if compact syntax misses trailing semicolon --- src/main/php/lang/ast/Parse.class.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 84b382db..77212b2e 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -430,7 +430,7 @@ public function __construct($tokens, $file= null, $scope= null) { $n->line= $this->token->line; $n->kind= 'return'; $statements= [$n]; - $this->token= $this->expect(';'); + $this->token= $this->next(';'); } else { // Regular function $this->token= $this->expect('{'); $statements= $this->statements(); @@ -462,7 +462,7 @@ public function __construct($tokens, $file= null, $scope= null) { $node->value[$variable]= $initial; if (',' === $this->token->value) { - $this->token= $this->expect(','); + $this->token= $this->advance(); } } } @@ -604,7 +604,7 @@ public function __construct($tokens, $file= null, $scope= null) { $cases[sizeof($cases) - 1]->body[]= $this->statement(); } } - $this->token= $this->expect('}'); + $this->token= $this->advance(); $node->value= new SwitchStatement($condition, $cases); $node->kind= 'switch'; @@ -753,10 +753,11 @@ public function __construct($tokens, $file= null, $scope= null) { $this->stmt('return', function($node) { if (';' === $this->token->value) { $expr= null; + $this->token= $this->advance(); } else { $expr= $this->expression(0); + $this->token= $this->next(';'); } - $this->token= $this->expect(';'); $node->value= $expr; $node->kind= 'return'; From 2403c8c46f68c70a3c72313b13120ef92900889c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 22:00:18 +0200 Subject: [PATCH 013/926] Change "{" (blocks) from prefix to statement --- src/main/php/lang/ast/Parse.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 77212b2e..18901b66 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -329,13 +329,6 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->prefix('{', function($node) { - $node->kind= 'block'; - $node->value= $this->statements(); - $this->token= new Node(self::symbol(';')); - return $node; - }); - $this->prefix('new', function($node) { $type= $this->token; $this->token= $this->advance(); @@ -492,6 +485,13 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); + $this->stmt('{', function($node) { + $node->kind= 'block'; + $node->value= $this->statements(); + $this->token= $this->advance(); + return $node; + }); + $this->prefix('echo', function($node) { $node->kind= 'echo'; $node->value= $this->arguments(';'); From 325ad594b6a26d7033ae5f934d0add6d48c6fb99 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 22:08:26 +0200 Subject: [PATCH 014/926] Switch loops, try/catch and annotations to continued parsing --- src/main/php/lang/ast/Parse.class.php | 47 ++++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 18901b66..b6aceb78 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -640,10 +640,10 @@ public function __construct($tokens, $file= null, $scope= null) { $this->stmt('do', function($node) { $block= $this->block(); - $this->token= $this->expect('while'); - $this->token= $this->expect('('); + $this->token= $this->next('while'); + $this->token= $this->next('('); $expression= $this->expression(0); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); $this->token= $this->next(';'); $node->value= new DoLoop($expression, $block); @@ -652,9 +652,9 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->stmt('while', function($node) { - $this->token= $this->expect('('); + $this->token= $this->next('('); $expression= $this->expression(0); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); $block= $this->block(); $node->value= new WhileLoop($expression, $block); @@ -663,13 +663,13 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->stmt('for', function($node) { - $this->token= $this->expect('('); + $this->token= $this->next('('); $init= $this->arguments(';'); - $this->token= $this->advance(';'); + $this->token= $this->next(';'); $cond= $this->arguments(';'); - $this->token= $this->advance(';'); + $this->token= $this->next(';'); $loop= $this->arguments(')'); - $this->token= $this->advance(')'); + $this->token= $this->next(')'); $block= $this->block(); @@ -679,7 +679,7 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->stmt('foreach', function($node) { - $this->token= $this->expect('('); + $this->token= $this->next('('); $expression= $this->expression(0); $this->token= $this->advance('as'); @@ -694,7 +694,7 @@ public function __construct($tokens, $file= null, $scope= null) { $value= $expr; } - $this->token= $this->expect(')'); + $this->token= $this->next(')'); $block= $this->block(); $node->value= new ForeachLoop($expression, $key, $value, $block); @@ -710,14 +710,14 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->stmt('try', function($node) { - $this->token= $this->expect('{'); + $this->token= $this->next('{'); $statements= $this->statements(); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); $catches= []; while ('catch' === $this->token->value) { $this->token= $this->advance(); - $this->token= $this->expect('('); + $this->token= $this->next('('); $types= []; while ('name' === $this->token->kind) { @@ -729,18 +729,18 @@ public function __construct($tokens, $file= null, $scope= null) { $variable= $this->token; $this->token= $this->advance(); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); - $this->token= $this->expect('{'); + $this->token= $this->next('{'); $catches[]= new CatchStatement($types, $variable->value, $this->statements()); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); } if ('finally' === $this->token->value) { $this->token= $this->advance(); - $this->token= $this->expect('{'); + $this->token= $this->next('{'); $finally= $this->statements(); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); } else { $finally= null; } @@ -790,9 +790,9 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); if ('(' === $this->token->value) { - $this->token= $this->expect('('); + $this->token= $this->next('('); $this->scope->annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); } else { $this->scope->annotations[$name]= null; } @@ -802,11 +802,12 @@ public function __construct($tokens, $file= null, $scope= null) { } else if ('>>' === $this->token->value) { break; } else { - $this->expect(', or >>', 'annotation'); + $this->next(', or >>', 'annotation'); + break; } } while (true); - $this->token= $this->expect('>>', 'annotation'); + $this->token= $this->advance(); $node->kind= 'annotation'; return $node; }); From 99e64d32ff5377fd9387edcd1e8f7c17b023fb8a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 22:56:39 +0200 Subject: [PATCH 015/926] Migrate class implements and interface extends to continued parsing --- src/main/php/lang/ast/Parse.class.php | 13 ++++++------- .../lang/ast/unittest/parse/ErrorsTest.class.php | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index b6aceb78..705f5dd0 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -802,10 +802,9 @@ public function __construct($tokens, $file= null, $scope= null) { } else if ('>>' === $this->token->value) { break; } else { - $this->next(', or >>', 'annotation'); - break; + $this->token= $this->next(', or >>', 'annotation'); } - } while (true); + } while (null !== $this->token->value); $this->token= $this->advance(); $node->kind= 'annotation'; @@ -838,9 +837,9 @@ public function __construct($tokens, $file= null, $scope= null) { } else if ('{' === $this->token->value) { break; } else { - $this->expect(', or {', 'interface parents'); + $this->token= $this->next(', or {', 'interface parents'); } - } while (true); + } while (null !== $this->token->value); } $this->token= $this->expect('{'); @@ -1090,9 +1089,9 @@ private function clazz($name, $modifiers= []) { } else if ('{' === $this->token->value) { break; } else { - $this->expect(', or {', 'interfaces list'); + $this->token= $this->next(', or {', 'interfaces list'); } - } while (true); + } while (null !== $this->token->value); } $this->token= $this->expect('{'); diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 02f1c913..7fd84fa7 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -56,7 +56,7 @@ public function unclosed_type() { public function missing_comma_in_implements() { $this->assertError( 'Expected ", or {", have "B" in interfaces list', - $this->parse('class A implements I B') + $this->parse('class A implements I B { }') ); } @@ -64,7 +64,7 @@ public function missing_comma_in_implements() { public function missing_comma_in_interface_parents() { $this->assertError( 'Expected ", or {", have "B" in interface parents', - $this->parse('interface I extends A B') + $this->parse('interface I extends A B { }') ); } From 50a8146ac11a792153668bf0520246caa9a803d9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 23:03:53 +0200 Subject: [PATCH 016/926] Migrate annotations to continued parsing --- src/main/php/lang/ast/Parse.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 705f5dd0..c07b5794 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -916,9 +916,9 @@ private function type0($optional) { } else if (')' === $this->token->value) { break; } else { - $this->expect(', or )', 'function type'); + $this->token= $this->next(', or )', 'function type'); } - } while (true); + } while (null !== $this->token->value); $this->token= $this->expect(')'); $this->token= $this->expect(':'); return new FunctionType($signature, $this->type(false)); @@ -983,9 +983,9 @@ private function parameters() { } else if ('>>' === $this->token->value) { break; } else { - $this->expect(', or >>', 'parameter annotation'); + $this->token= $this->next(', or >>', 'parameter annotation'); } - } while (true); + } while (null !== $this->token->value); $this->token= $this->expect('>>', 'parameter annotation'); } @@ -1325,9 +1325,9 @@ private function body() { } else if ('>>' === $this->token->value) { break; } else { - $this->expect(', or >>', 'annotations'); + $this->token= $this->next(', or >>', 'annotations'); } - } while (true); + } while (null !== $this->token->value); $this->token= $this->expect('>>'); } else if ($type= $this->type()) { continue; From 0a5568a0bdb25877b1a62c211103f8b5fbd3c702 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 23:07:12 +0200 Subject: [PATCH 017/926] Migrate blocks to cntinued parsing --- src/main/php/lang/ast/Parse.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index c07b5794..0f8a2ecd 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -790,7 +790,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->token= $this->advance(); if ('(' === $this->token->value) { - $this->token= $this->next('('); + $this->token= $this->advance(); $this->scope->annotations[$name]= $this->expression(0); $this->token= $this->next(')'); } else { @@ -833,7 +833,7 @@ public function __construct($tokens, $file= null, $scope= null) { $parents[]= $this->scope->resolve($this->token->value); $this->token= $this->advance(); if (',' === $this->token->value) { - $this->token= $this->expect(','); + $this->token= $this->advance(); } else if ('{' === $this->token->value) { break; } else { @@ -1046,9 +1046,9 @@ private function signature() { private function block() { if ('{' === $this->token->value) { - $this->token= $this->expect('{'); + $this->token= $this->advance(); $block= $this->statements(); - $this->token= $this->expect('}'); + $this->token= $this->next('}'); return $block; } else { return [$this->statement()]; @@ -1290,7 +1290,7 @@ private function body() { $this->token= $this->advance(); if ('=' === $this->token->value) { - $this->token= $this->expect('='); + $this->token= $this->advance(); $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); } else { $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); @@ -1313,9 +1313,9 @@ private function body() { $this->token= $this->advance(); if ('(' === $this->token->value) { - $this->token= $this->expect('('); + $this->token= $this->advance(); $annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); + $this->token= $this->next(')'); } else { $annotations[$name]= null; } From 5ee022b664e99a0c8c516b1b05715a21afcc357e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Jun 2018 23:13:20 +0200 Subject: [PATCH 018/926] Migrate array literals to continued parsing --- src/main/php/lang/ast/Parse.class.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 0f8a2ecd..87229137 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -319,11 +319,14 @@ public function __construct($tokens, $file= null, $scope= null) { $values[]= [null, $expr]; } - if (']' === $this->token->value) break; - $this->token= $this->expect(',', 'array literal'); + if (']' === $this->token->value) { + break; + } else { + $this->token= $this->next(',', 'array literal'); + } } - $this->token= $this->expect(']', 'array literal'); + $this->token= $this->next(']', 'array literal'); $node->kind= 'array'; $node->value= $values; return $node; @@ -1108,7 +1111,7 @@ private function arguments($end= ')') { while ($end !== $this->token->value) { $arguments[]= $this->expression(0, false); // Undefined arguments are OK if (',' === $this->token->value) { - $this->token= $this->expect(','); + $this->token= $this->advance(); } else if ($end === $this->token->value) { break; } else { @@ -1298,13 +1301,13 @@ private function body() { $body[$lookup]= $member; if (',' === $this->token->value) { - $this->token= $this->expect(','); + $this->token= $this->advance(); } } $modifiers= []; $annotations= []; $type= null; - $this->token= $this->expect(';', 'field declaration'); + $this->token= $this->next(';', 'field declaration'); } else if ('<<' === $this->token->symbol->id) { do { $this->token= $this->advance(); @@ -1328,7 +1331,7 @@ private function body() { $this->token= $this->next(', or >>', 'annotations'); } } while (null !== $this->token->value); - $this->token= $this->expect('>>'); + $this->token= $this->advance(); } else if ($type= $this->type()) { continue; } else { From 415fe7fa6d21f4dad73cf469ef44737adcbe8c74 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 29 Dec 2018 15:41:41 +0100 Subject: [PATCH 019/926] QA: Apidocs --- src/main/php/lang/ast/Errors.class.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Errors.class.php b/src/main/php/lang/ast/Errors.class.php index 16fa5003..04576bd5 100755 --- a/src/main/php/lang/ast/Errors.class.php +++ b/src/main/php/lang/ast/Errors.class.php @@ -8,7 +8,7 @@ class Errors extends IllegalStateException { /** * Creates a new error * - * @param lang.ast.Error $errors + * @param lang.ast.Error[] $errors * @param string $file */ public function __construct($errors, $file) { @@ -27,11 +27,18 @@ public function __construct($errors, $file) { } $message.= '}'; } + parent::__construct($message); $this->file= $file; $this->line= $errors[0]->getLine(); } + /** + * Returns diagnostics + * + * @param string $indent + * @return string + */ public function diagnostics($indent= '') { return str_replace("\n", "\n".$indent, $this->message); } From d2d17a7e06e6692838e29c12cc3a0b252395a881 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 29 Dec 2018 16:30:48 +0100 Subject: [PATCH 020/926] Fix PHP 5 compatibility --- src/main/php/lang/ast/CompilingClassloader.class.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index f493e372..ed5b2d89 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -169,6 +169,9 @@ public function loadClass0($class) { } catch (\Throwable $e) { unset(\xp::$cl[$class]); throw new ClassFormatException('Compiler error: '.$e->getMessage(), $e); + } catch (\Exception $e) { + unset(\xp::$cl[$class]); + throw new ClassFormatException('Compiler error: '.$e->getMessage(), $e); } finally { \xp::$cll--; unset(Compiled::$source[$uri]); From 2f50ebbc4ec560e4efd21e63ba79bdb733b341f5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 10:00:22 +0200 Subject: [PATCH 021/926] Prefer PHP 7.4 short method syntax instead of Hack language variant --- src/test/php/lang/ast/unittest/emit/EchoTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/VarargsTest.class.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index 6bccad27..b9e25fdf 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -14,7 +14,7 @@ private function assertEchoes($expected, $statement) { ob_start(); try { $this->run('class { - private function hello() ==> "Hello"; + private fn hello() => "Hello"; public function run() { '.$statement.' } }'); $this->assertEquals($expected, ob_get_contents()); diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 67c8987c..21a219c5 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -166,7 +166,7 @@ public function run($message) { #[@test, @expect(IllegalArgumentException::class)] public function throw_expression_with_compact_method() { $t= $this->type('class { - public function run() ==> throw new \\lang\\IllegalArgumentException("test"); + public fn run() => throw new \\lang\\IllegalArgumentException("test"); }'); $t->newInstance()->run(); } diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index 574422ec..cf65dc9f 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -92,7 +92,7 @@ public function run() { public $member= true; }; $member= new class() { - public function name() ==> "member"; + public fn name() => "member"; }; return $object?->{$member->name()}; } diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index 650019dd..cfdb4555 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -5,7 +5,7 @@ class VarargsTest extends EmittingTest { #[@test] public function vsprintf() { $r= $this->run('class { - private function format(string $format, ... $args) ==> vsprintf($format, $args); + private fn format(string $format, ... $args) => vsprintf($format, $args); public function run() { return $this->format("Hello %s", "Test"); @@ -18,7 +18,7 @@ public function run() { #[@test] public function list_of() { $r= $this->run('class { - private function listOf(string... $args) ==> $args; + private fn listOf(string... $args) => $args; public function run() { return $this->listOf("Hello", "Test"); From 155e8309484404b964d46d319b023d4f80b54b2e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 10:37:45 +0200 Subject: [PATCH 022/926] Raise warnings for use of hack style arrow functions --- ChangeLog.md | 5 +++++ src/main/php/lang/ast/Parse.class.php | 6 +++++- .../lang/ast/unittest/emit/CompactFunctionsTest.class.php | 1 + src/test/php/lang/ast/unittest/emit/LambdasTest.class.php | 3 +++ .../lang/ast/unittest/parse/CompactFunctionsTest.class.php | 2 ++ src/test/php/lang/ast/unittest/parse/LambdasTest.class.php | 3 +++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8164add4..e36f7745 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Changed compiler to emit deprecation warnings for Hack language style + arrow functions and compact methods using `==>`, instead advocating the + use of PHP 7.4 with the `fn` keyword. + (@thekid) + ## 2.13.0 / 2019-06-15 * Added preliminary PHP 8 support - see #62 (@thekid) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index e7d31398..33e2c6b3 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -158,6 +158,7 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->infix('==>', 80, function($node, $left) { + trigger_error('Hack language style arrow functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); $signature= new Signature([new Parameter($left->value, null)], null); if ('{' === $this->token->value) { @@ -272,8 +273,9 @@ public function __construct($tokens, $file= null, $scope= null) { $this->queue= array_merge($skipped, $this->queue); if (':' === $this->token->value || '==>' === $this->token->value) { - $node->kind= 'lambda'; + trigger_error('Hack language style arrow functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); + $node->kind= 'lambda'; $this->token= $this->advance(); $signature= $this->signature(); $this->token= $this->advance(); @@ -1257,6 +1259,8 @@ private function body() { $statements= null; $this->token= $this->expect(';'); } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' + trigger_error('Hack language style compact functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); + $n= new Node($this->token->symbol); $n->line= $this->token->line; $this->token= $this->advance(); diff --git a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php index d84a4228..cfafabfc 100755 --- a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php @@ -45,5 +45,6 @@ public function hacklang_variation_also_supported() { public function run() ==> "test"; }'); $this->assertEquals('test', $r); + \xp::gc(); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 82760eae..a1366d69 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -193,6 +193,7 @@ public function run() { }'); $this->assertEquals(2, $r(1)); + \xp::gc(); } #[@test] @@ -204,6 +205,7 @@ public function run() { }'); $this->assertEquals(2, $r(1)); + \xp::gc(); } #[@test] @@ -216,5 +218,6 @@ public function run() { }'); $this->assertEquals(3, $r); + \xp::gc(); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php index 38b9c202..2283f079 100755 --- a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php @@ -8,6 +8,7 @@ public function function_returning_null() { [['function' => ['a', [[], null], [['==>' => ['null' => 'null']]]]]], $this->parse('function a() ==> null;') ); + \xp::gc(); } #[@test] @@ -19,5 +20,6 @@ public function short_method() { ], [], null]]], $this->parse('class A { public function a() ==> true; }') ); + \xp::gc(); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php index 234ab6ae..85cf877b 100755 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php @@ -9,6 +9,7 @@ public function short_closure() { [['(' => [[[['a', false, null, false, null, null, []]], null], $block]]], $this->parse('($a) ==> $a + 1;') ); + \xp::gc(); } #[@test] @@ -20,6 +21,7 @@ public function short_closure_as_arg() { ]]]], $this->parse('exec(($a) ==> $a + 1);') ); + \xp::gc(); } #[@test] @@ -29,5 +31,6 @@ public function short_closure_with_braces() { [['(' => [[[['a', false, null, false, null, null, []]], null], [['return' => $block]]]]], $this->parse('($a) ==> { return $a + 1; };') ); + \xp::gc(); } } \ No newline at end of file From 4ca332e0776118eba7fd694ff37a293485ca8c33 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 10:41:47 +0200 Subject: [PATCH 023/926] Extract emitting warnings into dedicated method --- src/main/php/lang/ast/Parse.class.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 33e2c6b3..ae014152 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -158,9 +158,9 @@ public function __construct($tokens, $file= null, $scope= null) { }); $this->infix('==>', 80, function($node, $left) { - trigger_error('Hack language style arrow functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); - $signature= new Signature([new Parameter($left->value, null)], null); + $this->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + $signature= new Signature([new Parameter($left->value, null)], null); if ('{' === $this->token->value) { $this->token= $this->expect('{'); $statements= $this->statements(); @@ -273,7 +273,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->queue= array_merge($skipped, $this->queue); if (':' === $this->token->value || '==>' === $this->token->value) { - trigger_error('Hack language style arrow functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); + $this->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); $node->kind= 'lambda'; $this->token= $this->advance(); @@ -1259,7 +1259,7 @@ private function body() { $statements= null; $this->token= $this->expect(';'); } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - trigger_error('Hack language style compact functions are deprecated, please use `fn` syntax instead on line '.$this->token->line); + $this->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); $n= new Node($this->token->symbol); $n->line= $this->token->line; @@ -1539,6 +1539,18 @@ private function raise($message, $context= null) { throw new Error($message, $this->file, $this->token->line); } + /** + * Emit a warning + * + * @param string $error + * @param string $context + * @return void + */ + private function warn($message, $context= null) { + $context && $message.= ' ('.$context.')'; + trigger_error($message.' in '.$this->file.' on line '.$this->token->line); + } + /** * Expect a given token, raise an error if another is encountered * From 04e415a26a3ee74e0df5f88f029de10b47ff3d76 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 11:13:40 +0200 Subject: [PATCH 024/926] Document merging of PR #45 --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index e36f7745..d87eb8eb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #45 - Multiple errors - @thekid * Changed compiler to emit deprecation warnings for Hack language style arrow functions and compact methods using `==>`, instead advocating the use of PHP 7.4 with the `fn` keyword. From 062e4bd97c74bbfbf3bf21dd9925f2f8cdcfe0fb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 13:42:17 +0200 Subject: [PATCH 025/926] Refactor parsing type body into functions --- src/main/php/lang/ast/Parse.class.php | 406 +++++++++++++------------- 1 file changed, 209 insertions(+), 197 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 12cdfb6c..c0cce0bc 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -42,6 +42,7 @@ class Parse { private $tokens, $file, $token, $scope; private $comment= null; + private $body= []; private $queue= []; private $errors= []; @@ -867,7 +868,7 @@ public function __construct($tokens, $file= null, $scope= null) { } $this->token= $this->expect('{'); - $body= $this->body(); + $body= $this->typeBody(); $this->token= $this->expect('}'); $node->value= new InterfaceDeclaration([], $type, $parents, $body, $this->scope->annotations, $comment); @@ -883,7 +884,7 @@ public function __construct($tokens, $file= null, $scope= null) { $this->comment= null; $this->token= $this->expect('{'); - $body= $this->body(); + $body= $this->typeBody(); $this->token= $this->expect('}'); $node->value= new TraitDeclaration([], $type, $body, $this->scope->annotations, $comment); @@ -905,6 +906,164 @@ public function __construct($tokens, $file= null, $scope= null) { $node->kind= 'using'; return $node; }); + + $this->body('use', function(&$body, $annotations, $modifiers) { + $member= new Node($this->token->symbol); + $member->kind= 'use'; + $member->line= $this->token->line; + + $this->token= $this->advance(); + $types= []; + do { + $types[]= $this->scope->resolve($this->token->value); + $this->token= $this->advance(); + if (',' === $this->token->value) { + $this->token= $this->advance(); + continue; + } else { + break; + } + } while ($this->token->value); + + $aliases= []; + if ('{' === $this->token->value) { + $this->token= $this->advance(); + while ('}' !== $this->token->value) { + $method= $this->token->value; + $this->token= $this->advance(); + if ('::' === $this->token->value) { + $this->token= $this->advance(); + $method= $this->scope->resolve($method).'::'.$this->token->value; + $this->token= $this->advance(); + } + $this->token= $this->expect('as'); + $alias= $this->token->value; + $this->token= $this->advance(); + $this->token= $this->expect(';'); + $aliases[$method]= $alias; + } + $this->token= $this->expect('}'); + } else { + $this->token= $this->expect(';'); + } + + $member->value= new UseExpression($types, $aliases); + $body[]= $member; + }); + + $this->body('const', function(&$body, $annotations, $modifiers) { + $n= new Node($this->token->symbol); + $n->kind= 'const'; + $this->token= $this->advance(); + + $type= null; + while (';' !== $this->token->value) { + $member= clone $n; + $member->line= $this->token->line; + $first= $this->token; + $this->token= $this->advance(); + + // Untyped `const T = 5` vs. typed `const int T = 5` + if ('=' === $this->token->value) { + $name= $first->value; + } else { + $this->queue[]= $first; + $this->queue[]= $this->token; + $this->token= $first; + + $type= $this->type(false); + $this->token= $this->advance(); + $name= $this->token->value; + $this->token= $this->advance(); + } + + if (isset($body[$name])) { + $this->raise('Cannot redeclare constant '.$name); + } + + $this->token= $this->expect('='); + $member->value= new Constant($modifiers, $name, $type, $this->expression(0)); + $body[$name]= $member; + if (',' === $this->token->value) { + $this->token= $this->expect(','); + } + } + $this->token= $this->expect(';', 'constant declaration'); + }); + + $this->body('@variable', function(&$body, $annotations, $modifiers) { + $this->properties($body, $annotations, $modifiers, null); + }); + + $this->body('fn', function(&$body, $annotations, $modifiers) { + $member= new Node($this->token->symbol); + $member->kind= 'method'; + $member->line= $this->token->line; + $comment= $this->comment; + $this->comment= null; + + $this->token= $this->advance(); + $name= $this->token->value; + $lookup= $name.'()'; + if (isset($body[$lookup])) { + $this->raise('Cannot redeclare method '.$lookup); + } + + $this->token= $this->advance(); + $signature= $this->signature(); + + $this->token= $this->expect('=>'); + $n= new Node($this->token->symbol); + $n->line= $this->token->line; + $n->value= $this->expressionWithThrows(0); + $n->kind= 'return'; + $this->token= $this->expect(';'); + + $member->value= new Method($modifiers, $name, $signature, [$n], $annotations, $comment); + $body[$lookup]= $member; + }); + + $this->body('function', function(&$body, $annotations, $modifiers) { + $member= new Node($this->token->symbol); + $member->kind= 'method'; + $member->line= $this->token->line; + $comment= $this->comment; + $this->comment= null; + + $this->token= $this->advance(); + $name= $this->token->value; + $lookup= $name.'()'; + if (isset($body[$lookup])) { + $this->raise('Cannot redeclare method '.$lookup); + } + + $this->token= $this->advance(); + $signature= $this->signature(); + + if ('{' === $this->token->value) { // Regular body + $this->token= $this->advance(); + $statements= $this->statements(); + $this->token= $this->expect('}'); + } else if (';' === $this->token->value) { // Abstract or interface method + $statements= null; + $this->token= $this->expect(';'); + } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' + $this->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); + + $n= new Node($this->token->symbol); + $n->line= $this->token->line; + $this->token= $this->advance(); + $n->value= $this->expressionWithThrows(0); + $n->kind= 'return'; + $statements= [$n]; + $this->token= $this->expect(';'); + } else { + $this->token= $this->expect('{, ; or ==>', 'method declaration'); + } + + $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); + $body[$lookup]= $member; + }); } private function type($optional= true) { @@ -981,6 +1140,45 @@ private function type0($optional) { } } + private function properties(&$body, $annotations, $modifiers, $type) { + $n= new Node($this->token->symbol); + $n->kind= 'property'; + $comment= $this->comment; + $this->comment= null; + + while (';' !== $this->token->value) { + $member= clone $n; + $member->line= $this->token->line; + + // Untyped `$a` vs. typed `int $a` + if ('variable' === $this->token->kind) { + $name= $this->token->value; + } else { + $type= $this->type(false); + $name= $this->token->value; + } + + $lookup= '$'.$name; + if (isset($body[$lookup])) { + $this->raise('Cannot redeclare property '.$lookup); + } + + $this->token= $this->advance(); + if ('=' === $this->token->value) { + $this->token= $this->advance(); + $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); + } else { + $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); + } + + $body[$lookup]= $member; + if (',' === $this->token->value) { + $this->token= $this->advance(); + } + } + $this->token= $this->next(';', 'field declaration'); + } + private function parameters() { static $promotion= ['private' => true, 'protected' => true, 'public' => true]; @@ -1119,7 +1317,7 @@ private function clazz($name, $modifiers= []) { } $this->token= $this->expect('{'); - $body= $this->body(); + $body= $this->typeBody(); $this->token= $this->expect('}'); $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); @@ -1150,7 +1348,7 @@ private function arguments($end= ')') { * - `[modifiers] const int T = 5` * - `[modifiers] function t(): int { }` */ - private function body() { + private function typeBody() { static $modifier= [ 'private' => true, 'protected' => true, @@ -1163,204 +1361,14 @@ private function body() { $body= []; $modifiers= []; $annotations= []; - $type= null; while ('}' !== $this->token->value) { if (isset($modifier[$this->token->value])) { $modifiers[]= $this->token->value; $this->token= $this->advance(); - } else if ('use' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'use'; - $member->line= $this->token->line; - - $this->token= $this->advance(); - $types= []; - do { - $types[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->advance(); - continue; - } else { - break; - } - } while ($this->token->value); - - $aliases= []; - if ('{' === $this->token->value) { - $this->token= $this->advance(); - while ('}' !== $this->token->value) { - $method= $this->token->value; - $this->token= $this->advance(); - if ('::' === $this->token->value) { - $this->token= $this->advance(); - $method= $this->scope->resolve($method).'::'.$this->token->value; - $this->token= $this->advance(); - } - $this->token= $this->expect('as'); - $alias= $this->token->value; - $this->token= $this->advance(); - $this->token= $this->expect(';'); - $aliases[$method]= $alias; - } - $this->token= $this->expect('}'); - } else { - $this->token= $this->expect(';'); - } - - $member->value= new UseExpression($types, $aliases); - $body[]= $member; - } else if ('fn' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->advance(); - $name= $this->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); - } - - $this->token= $this->advance(); - $signature= $this->signature(); - - $this->token= $this->expect('=>'); - - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $n->value= $this->expressionWithThrows(0); - $n->kind= 'return'; - $statements= [$n]; - $this->token= $this->expect(';'); - - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; + } else if ($f= $this->body[$this->token->value] ?? $this->body['@'.$this->token->kind] ?? null) { + $f($body, $annotations, $modifiers); $modifiers= []; $annotations= []; - } else if ('function' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->advance(); - $name= $this->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); - } - - $this->token= $this->advance(); - $signature= $this->signature(); - - if ('{' === $this->token->value) { // Regular body - $this->token= $this->advance(); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else if (';' === $this->token->value) { // Abstract or interface method - $statements= null; - $this->token= $this->expect(';'); - } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - $this->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); - - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $this->token= $this->advance(); - $n->value= $this->expressionWithThrows(0); - $n->kind= 'return'; - $statements= [$n]; - $this->token= $this->expect(';'); - } else { - $this->token= $this->expect('{, ; or ==>', 'method declaration'); - } - - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; - $modifiers= []; - $annotations= []; - } else if ('const' === $this->token->value) { - $n= new Node($this->token->symbol); - $n->kind= 'const'; - $this->token= $this->advance(); - - $type= null; - while (';' !== $this->token->value) { - $member= clone $n; - $member->line= $this->token->line; - $first= $this->token; - $this->token= $this->advance(); - - // Untyped `const T = 5` vs. typed `const int T = 5` - if ('=' === $this->token->value) { - $name= $first->value; - } else { - $this->queue[]= $first; - $this->queue[]= $this->token; - $this->token= $first; - - $type= $this->type(false); - $this->token= $this->advance(); - $name= $this->token->value; - $this->token= $this->advance(); - } - - if (isset($body[$name])) { - $this->raise('Cannot redeclare constant '.$name); - } - - $this->token= $this->expect('='); - $member->value= new Constant($modifiers, $name, $type, $this->expression(0)); - $body[$name]= $member; - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } - } - $this->token= $this->expect(';', 'constant declaration'); - $modifiers= []; - } else if ('variable' === $this->token->kind) { - $n= new Node($this->token->symbol); - $n->kind= 'property'; - $comment= $this->comment; - $this->comment= null; - - while (';' !== $this->token->value) { - $member= clone $n; - $member->line= $this->token->line; - - // Untyped `$a` vs. typed `int $a` - if ('variable' === $this->token->kind) { - $name= $this->token->value; - } else { - $type= $this->type(false); - $name= $this->token->value; - } - - $lookup= '$'.$name; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare property '.$lookup); - } - - $this->token= $this->advance(); - if ('=' === $this->token->value) { - $this->token= $this->advance(); - $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); - } else { - $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); - } - - $body[$lookup]= $member; - if (',' === $this->token->value) { - $this->token= $this->advance(); - } - } - $modifiers= []; - $annotations= []; - $type= null; - $this->token= $this->next(';', 'field declaration'); } else if ('<<' === $this->token->symbol->id) { do { $this->token= $this->advance(); @@ -1386,7 +1394,7 @@ private function body() { } while (null !== $this->token->value); $this->token= $this->advance(); } else if ($type= $this->type()) { - continue; + $this->properties($body, $annotations, $modifiers, $type); } else { $this->raise(sprintf( 'Expected a type, modifier, property, annotation, method or "}", have "%s"', @@ -1542,6 +1550,10 @@ private function suffix($id, $bp, $led= null) { }; return $suffix; } + + private function body($id, $func) { + $this->body[$id]= $func; + } // }}} /** From c146b8814bbe22dff50f3d8d442aabfd3849ad40 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 14:02:37 +0200 Subject: [PATCH 026/926] Extract features into their own syntax classes --- .../lang/ast/CompilingClassloader.class.php | 11 +++++- src/main/php/lang/ast/Parse.class.php | 32 +--------------- .../lang/ast/syntax/CompactMethods.class.php | 37 +++++++++++++++++++ .../ast/unittest/emit/EmittingTest.class.php | 9 +++++ 4 files changed, 58 insertions(+), 31 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/CompactMethods.class.php diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index ed5b2d89..a2e42a70 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -7,18 +7,23 @@ use lang\ElementNotFoundException; use lang\IClassLoader; use lang\XPClass; +use lang\reflect\Package; class CompilingClassLoader implements IClassLoader { const EXTENSION = '.php'; private static $instance= []; + private $syntax= []; private $version; /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); - Compiled::$emit[$this->version]= $emit; + foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { + $this->synax[]= $class->newInstance(); + } + Compiled::$emit[$this->version]= $emit; stream_wrapper_register($this->version, Compiled::class); } @@ -204,6 +209,10 @@ public function loadClassBytes($class) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); + foreach ($this->syntax as $syntax) { + $syntax->parse($parse); + } + $emitter= $this->emit->newInstance($declaration); foreach (Transformations::registered() as $kind => $function) { $emitter->transform($kind, $function); diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index c0cce0bc..2e63e720 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -995,34 +995,6 @@ public function __construct($tokens, $file= null, $scope= null) { $this->properties($body, $annotations, $modifiers, null); }); - $this->body('fn', function(&$body, $annotations, $modifiers) { - $member= new Node($this->token->symbol); - $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->advance(); - $name= $this->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); - } - - $this->token= $this->advance(); - $signature= $this->signature(); - - $this->token= $this->expect('=>'); - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $n->value= $this->expressionWithThrows(0); - $n->kind= 'return'; - $this->token= $this->expect(';'); - - $member->value= new Method($modifiers, $name, $signature, [$n], $annotations, $comment); - $body[$lookup]= $member; - }); - $this->body('function', function(&$body, $annotations, $modifiers) { $member= new Node($this->token->symbol); $member->kind= 'method'; @@ -1551,8 +1523,8 @@ private function suffix($id, $bp, $led= null) { return $suffix; } - private function body($id, $func) { - $this->body[$id]= $func; + public function body($id, $func) { + $this->body[$id]= $func->bindTo($this, self::class); } // }}} diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php new file mode 100755 index 00000000..37e20a48 --- /dev/null +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -0,0 +1,37 @@ +body('fn', function(&$body, $annotations, $modifiers) { + $member= new Node($this->token->symbol); + $member->kind= 'method'; + $member->line= $this->token->line; + $comment= $this->comment; + $this->comment= null; + + $this->token= $this->advance(); + $name= $this->token->value; + $lookup= $name.'()'; + if (isset($body[$lookup])) { + $this->raise('Cannot redeclare method '.$lookup); + } + + $this->token= $this->advance(); + $signature= $this->signature(); + + $this->token= $this->expect('=>'); + $return= new Node($this->token->symbol); + $return->line= $this->token->line; + $return->value= $this->expressionWithThrows(0); + $return->kind= 'return'; + $this->token= $this->expect(';'); + + $member->value= new Method($modifiers, $name, $signature, [$return], $annotations, $comment); + $body[$lookup]= $member; + }); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 65dd1434..2f4d3ca1 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -7,14 +7,19 @@ use lang\ast\Node; use lang\ast\Parse; use lang\ast\Tokens; +use lang\reflect\Package; use text\StringTokenizer; abstract class EmittingTest extends \unittest\TestCase { private static $cl; + private static $syntax= []; private static $id= 0; static function __static() { self::$cl= DynamicClassLoader::instanceFor(self::class); + foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { + self::$syntax[]= $class->newInstance(); + } } /** @@ -27,6 +32,10 @@ protected function type($code) { $name= 'T'.(self::$id++); $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); + foreach (self::$syntax as $syntax) { + $syntax->parse($parse); + } + $out= new MemoryOutputStream(); $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); $emit->emit($parse->execute()); From 988e9fb8cb568e11859a92b0b43f4ba9ffff3c0a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Jun 2019 14:12:03 +0200 Subject: [PATCH 027/926] Rename parse() -> setup() to be more concise about method's purpose --- src/main/php/lang/ast/CompilingClassloader.class.php | 4 ++-- src/main/php/lang/ast/syntax/CompactMethods.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index a2e42a70..bbfd1c79 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -20,7 +20,7 @@ class CompilingClassLoader implements IClassLoader { private function __construct($emit) { $this->version= $emit->getSimpleName(); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - $this->synax[]= $class->newInstance(); + $this->syntax[]= $class->newInstance(); } Compiled::$emit[$this->version]= $emit; @@ -210,7 +210,7 @@ public function loadClassBytes($class) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); foreach ($this->syntax as $syntax) { - $syntax->parse($parse); + $syntax->setup($parse); } $emitter= $this->emit->newInstance($declaration); diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php index 37e20a48..1837c6e7 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -5,7 +5,7 @@ class CompactMethods { - public function parse($parser) { + public function setup($parser) { $parser->body('fn', function(&$body, $annotations, $modifiers) { $member= new Node($this->token->symbol); $member->kind= 'method'; diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 2f4d3ca1..46c96dd7 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -9,8 +9,9 @@ use lang\ast\Tokens; use lang\reflect\Package; use text\StringTokenizer; +use unittest\TestCase; -abstract class EmittingTest extends \unittest\TestCase { +abstract class EmittingTest extends TestCase { private static $cl; private static $syntax= []; private static $id= 0; @@ -33,7 +34,7 @@ protected function type($code) { $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); foreach (self::$syntax as $syntax) { - $syntax->parse($parse); + $syntax->setup($parse); } $out= new MemoryOutputStream(); From b1b16648f0291ae014438af7fde01b1a02ecf7e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 18 Jun 2019 11:20:18 +0200 Subject: [PATCH 028/926] Link to #65 [skip ci] --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index d87eb8eb..827c7fb0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,7 @@ XP Compiler ChangeLog * Merged PR #45 - Multiple errors - @thekid * Changed compiler to emit deprecation warnings for Hack language style arrow functions and compact methods using `==>`, instead advocating the - use of PHP 7.4 with the `fn` keyword. + use of PHP 7.4 with the `fn` keyword; see issue #65 (@thekid) ## 2.13.0 / 2019-06-15 From 479bcb728d38aeb2ddfe39c65161e786b39e06c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 18 Jun 2019 11:29:05 +0200 Subject: [PATCH 029/926] Restore PHP 5.6 compatibility --- src/main/php/lang/ast/Parse.class.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 2e63e720..4ffe33d3 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1337,7 +1337,10 @@ private function typeBody() { if (isset($modifier[$this->token->value])) { $modifiers[]= $this->token->value; $this->token= $this->advance(); - } else if ($f= $this->body[$this->token->value] ?? $this->body['@'.$this->token->kind] ?? null) { + } else if (isset($this->body[$k= $this->token->value]) + ? ($f= $this->body[$k]) + : (isset($this->body[$k= '@'.$this->token->kind]) ? ($f= $this->body[$k]) : null) + ) { $f($body, $annotations, $modifiers); $modifiers= []; $annotations= []; From d3e1d6e843fa099700f21bef765cd52649775b3a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:08:35 +0200 Subject: [PATCH 030/926] Use xp-framework/tokenize 8.1.0, which is compatible w/ PHP 7.4 --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 073135fa..a17b52ae 100755 --- a/composer.json +++ b/composer.json @@ -7,16 +7,13 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", - "xp-framework/tokenize": "^8.0", + "xp-framework/tokenize": "^8.1", "xp-framework/ast": "^1.3", "php" : ">=5.6.0" }, "require-dev" : { "xp-framework/unittest": "^9.3" }, - "suggest" : { - "xp-framework/core": "^9.4" - }, "bin": ["bin/xp.xp-framework.compiler.compile"], "autoload" : { "files" : ["src/main/php/autoload.php"] From 97a8e921e0f2b0c3a471329d433eedc6d9786df9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:10:46 +0200 Subject: [PATCH 031/926] Refrain from using curly braces for string offset access --- src/main/php/lang/ast/Scope.class.php | 2 +- src/main/php/lang/ast/Tokens.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Scope.class.php b/src/main/php/lang/ast/Scope.class.php index 4b0c076a..92be8847 100755 --- a/src/main/php/lang/ast/Scope.class.php +++ b/src/main/php/lang/ast/Scope.class.php @@ -62,7 +62,7 @@ public function import($name, $alias= null) { public function resolve($name) { if (isset(self::$reserved[$name])) { return $name; - } else if ('\\' === $name{0}) { + } else if ('\\' === $name[0]) { return $name; } else if (isset($this->imports[$name])) { return $this->imports[$name]; diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index 1f2ecf72..062b00f8 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -92,7 +92,7 @@ public function getIterator() { do { $t= $this->source->nextToken('/'); $comment.= $t; - } while ('*' !== $t{strlen($t)- 1} && $this->source->hasMoreTokens()); + } while ('*' !== $t[strlen($t)- 1] && $this->source->hasMoreTokens()); $comment.= $this->source->nextToken('/'); yield 'comment' => [trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 1, -2))), $line]; $line+= substr_count($comment, "\n"); From ede28f5d7614aabad4fb842ec4e955782086bb33 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:10:57 +0200 Subject: [PATCH 032/926] Implement stream_set_option() --- src/main/php/lang/ast/Compiled.class.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 8902deed..c1bf9a6d 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -88,8 +88,15 @@ public function stream_close() { // NOOP } - /** @return void */ - public function stream_flush() { - // NOOP + /** + * Stream wrapper method stream_set_option + * + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2) { + return true; } } \ No newline at end of file From d5839223f1bffca00767c65ef164798ca4f555cb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:17:34 +0200 Subject: [PATCH 033/926] Use xp-framework/ast 1.4.0, which is compatible w/ PHP 7.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a17b52ae..bd655a37 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "^1.3", + "xp-framework/ast": "^1.4", "php" : ">=5.6.0" }, "require-dev" : { From d553c3b0c8d78b8ff7ea30dd27e530a05ccb8458 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:19:11 +0200 Subject: [PATCH 034/926] Run tests with PHP 7.4 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ed032f2f..3bee7933 100755 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4snapshot - nightly matrix: @@ -17,7 +18,7 @@ matrix: - php: nightly before_script: - - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-master.sh > xp-run + - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run - composer install --prefer-dist - echo "vendor/autoload.php" > composer.pth From 12efd33006b74d67e9180ca611055f1097eb0f3d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:20:18 +0200 Subject: [PATCH 035/926] Document PHP 7.4 compatiblity --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 827c7fb0..8e1edbd5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Made compatible with PHP 7.4 - refrain using `{}` for string offsets + (@thekid) * Merged PR #45 - Multiple errors - @thekid * Changed compiler to emit deprecation warnings for Hack language style arrow functions and compact methods using `==>`, instead advocating the From 09947daa34eaca227a808b6016357ef07c7c767a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:31:50 +0200 Subject: [PATCH 036/926] Handle case when an empty name is passed to resolve() --- src/main/php/lang/ast/Scope.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Scope.class.php b/src/main/php/lang/ast/Scope.class.php index 92be8847..ecf95a10 100755 --- a/src/main/php/lang/ast/Scope.class.php +++ b/src/main/php/lang/ast/Scope.class.php @@ -60,7 +60,9 @@ public function import($name, $alias= null) { * @return string */ public function resolve($name) { - if (isset(self::$reserved[$name])) { + if (null === $name) { + return ''; + } else if (isset(self::$reserved[$name])) { return $name; } else if ('\\' === $name[0]) { return $name; From cce2d361a680248555179dc4e26ffb51cbfb8c8f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:45:36 +0200 Subject: [PATCH 037/926] Release 3.0.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 8e1edbd5..cc3e4bf7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 3.0.0 / 2019-08-10 + * Made compatible with PHP 7.4 - refrain using `{}` for string offsets (@thekid) * Merged PR #45 - Multiple errors - @thekid From b3ce649ae98bef6c5a6ef2265c6e3547f7456392 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:48:06 +0200 Subject: [PATCH 038/926] Drop HHVM badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ba02bb9d..2d6a194f 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ XP Compiler [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) [![Required PHP 5.6+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-5_6plus.png)](http://php.net/) [![Supports PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.png)](http://php.net/) -[![Supports HHVM 3.20+](https://raw.githubusercontent.com/xp-framework/web/master/static/hhvm-3_20plus.png)](http://hhvm.com/) [![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) Compiles future PHP to today's PHP. From 28339852db385582fac89473c51e817873635c4f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Aug 2019 16:51:26 +0200 Subject: [PATCH 039/926] Add PHP 7.4 to the list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d6a194f..dd5880e1 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 5.6. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 5.6. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php Date: Sun, 1 Sep 2019 13:30:03 +0200 Subject: [PATCH 040/926] Reset modifiers after property list parsing --- src/main/php/lang/ast/Parse.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 4ffe33d3..616400bc 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1370,6 +1370,7 @@ private function typeBody() { $this->token= $this->advance(); } else if ($type= $this->type()) { $this->properties($body, $annotations, $modifiers, $type); + $modifiers= []; } else { $this->raise(sprintf( 'Expected a type, modifier, property, annotation, method or "}", have "%s"', From 1cfdb5dae2af19a0a86bd4360fab3b33d5d27e5c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 13:37:59 +0200 Subject: [PATCH 041/926] Make it possible to influence emitter from syntax --- .../lang/ast/CompilingClassloader.class.php | 5 +- src/main/php/lang/ast/Emitter.class.php | 52 ++++++++----------- src/main/php/lang/ast/Parse.class.php | 37 ++++++------- .../lang/ast/syntax/CompactMethods.class.php | 2 +- src/main/php/lang/ast/syntax/Using.class.php | 46 ++++++++++++++++ .../php/xp/compiler/CompileRunner.class.php | 10 ++++ .../ast/unittest/emit/EmittingTest.class.php | 11 ++-- 7 files changed, 107 insertions(+), 56 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/Using.class.php diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index bbfd1c79..a4c0127b 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -209,11 +209,12 @@ public function loadClassBytes($class) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); + $emitter= $this->emit->newInstance($declaration); + foreach ($this->syntax as $syntax) { - $syntax->setup($parse); + $syntax->setup($parse, $emitter); } - $emitter= $this->emit->newInstance($declaration); foreach (Transformations::registered() as $kind => $function) { $emitter->transform($kind, $function); } diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 16e8aef7..a3b8d791 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -14,10 +14,11 @@ abstract class Emitter { protected $line= 1; protected $meta= []; protected $unsupported= []; - protected $transformations= []; protected $locals= []; protected $stack= []; + private $emit= []; + /** * Selects the correct emitter for a given runtime * @@ -44,7 +45,24 @@ public function __construct($out) { } public function transform($kind, $function) { - $this->transformations[$kind]= $function; + $this->emit[$kind]= function($arg) use($function) { + foreach ($function($arg) as $n) { + $this->{'emit'.$n->kind}($n->value); + } + }; + return $this; + } + + /** + * Emit nodes of a certain kind using the given emitter function. Overwrites + * any previous handling, including builtin behavior! + * + * @param string $kind + * @param function(lang.ast.Node): void $function + * @return self + */ + public function handle($kind, $function) { + $this->emit[$kind]= $function->bindTo($this, self::class); return $this; } @@ -848,30 +866,6 @@ protected function emitFrom($from) { $this->emit($from); } - protected function emitUsing($using) { - $variables= []; - foreach ($using->arguments as $expression) { - switch ($expression->kind) { - case 'variable': $variables[]= '$'.$expression->value; break; - case 'assignment': $variables[]= '$'.$expression->value->variable->value; break; - default: $temp= $this->temp(); $variables[]= $temp; $this->out->write($temp.'='); - } - $this->emit($expression); - $this->out->write(';'); - } - - $this->out->write('try {'); - $this->emit($using->body); - - $this->out->write('} finally {'); - foreach ($variables as $variable) { - $this->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); - $this->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); - $this->out->write('unset('.$variable.');'); - } - $this->out->write('}'); - } - public function emit($arg) { if ($arg instanceof Element) { if ($arg->line > $this->line) { @@ -879,10 +873,8 @@ public function emit($arg) { $this->line= $arg->line; } - if (isset($this->transformations[$arg->kind])) { - foreach ($this->transformations[$arg->kind]($arg) as $n) { - $this->{'emit'.$n->kind}($n->value); - } + if (isset($this->emit[$arg->kind])) { + $this->emit[$arg->kind]($arg); } else { $this->{'emit'.$arg->kind}($arg->value); } diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 616400bc..961a5deb 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -893,20 +893,6 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->stmt('using', function($node) { - $this->token= $this->expect('('); - $arguments= $this->arguments(); - $this->token= $this->expect(')'); - - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - - $node->value= new UsingStatement($arguments, $statements); - $node->kind= 'using'; - return $node; - }); - $this->body('use', function(&$body, $annotations, $modifiers) { $member= new Node($this->token->symbol); $member->kind= 'use'; @@ -1461,12 +1447,6 @@ private static function constant($id, $value) { return $const; } - private function stmt($id, $func) { - $stmt= self::symbol($id); - $stmt->std= $func; - return $stmt; - } - private function assignment($id) { $infix= self::symbol($id, 10); $infix->led= function($node, $left) use($id) { @@ -1527,6 +1507,23 @@ private function suffix($id, $bp, $led= null) { return $suffix; } + /** + * Statement + * + * @param string $id + * @param function(lang.ast.Node): lang.ast.Node + */ + public function stmt($id, $func) { + $stmt= self::symbol($id); + $stmt->std= $func->bindTo($this, self::class); + } + + /** + * Type body parsing + * + * @param string $id + * @param function([:string], [:string], string[]): void + */ public function body($id, $func) { $this->body[$id]= $func->bindTo($this, self::class); } diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php index 1837c6e7..428e8508 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -5,7 +5,7 @@ class CompactMethods { - public function setup($parser) { + public function setup($parser, $emitter) { $parser->body('fn', function(&$body, $annotations, $modifiers) { $member= new Node($this->token->symbol); $member->kind= 'method'; diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php new file mode 100755 index 00000000..0ac89593 --- /dev/null +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -0,0 +1,46 @@ +stmt('using', function($node) { + $this->token= $this->expect('('); + $arguments= $this->arguments(); + $this->token= $this->expect(')'); + + $this->token= $this->expect('{'); + $statements= $this->statements(); + $this->token= $this->expect('}'); + + $node->value= new UsingStatement($arguments, $statements); + $node->kind= 'using'; + return $node; + }); + + $emitter->handle('using', function($node) { + $variables= []; + foreach ($node->value->arguments as $expression) { + switch ($expression->kind) { + case 'variable': $variables[]= '$'.$expression->value; break; + case 'assignment': $variables[]= '$'.$expression->value->variable->value; break; + default: $temp= $this->temp(); $variables[]= $temp; $this->out->write($temp.'='); + } + $this->emit($expression); + $this->out->write(';'); + } + + $this->out->write('try {'); + $this->emit($node->value->body); + + $this->out->write('} finally {'); + foreach ($variables as $variable) { + $this->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); + $this->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); + $this->out->write('unset('.$variable.');'); + } + $this->out->write('}'); + }); + } +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index b3080c14..7928d388 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -7,6 +7,7 @@ use lang\ast\Parse; use lang\ast\Tokens; use lang\ast\transform\Transformations; +use lang\reflect\Package; use text\StreamTokenizer; use util\cmd\Console; use util\profiling\Timer; @@ -48,6 +49,11 @@ public static function main(array $args) { return 2; } + $syntaxes= []; + foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { + $syntaxes[]= $class->newInstance(); + } + $target= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; $in= $out= '-'; for ($i= 0; $i < sizeof($args); $i++) { @@ -81,6 +87,10 @@ public static function main(array $args) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); + foreach ($syntaxes as $syntax) { + $syntax->setup($parse, $emitter); + } + foreach (Transformations::registered() as $kind => $function) { $emitter->transform($kind, $function); } diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 46c96dd7..be5b1627 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -7,6 +7,7 @@ use lang\ast\Node; use lang\ast\Parse; use lang\ast\Tokens; +use lang\ast\transform\Transformations; use lang\reflect\Package; use text\StringTokenizer; use unittest\TestCase; @@ -31,14 +32,18 @@ static function __static() { */ protected function type($code) { $name= 'T'.(self::$id++); + $out= new MemoryOutputStream(); $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); + $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); foreach (self::$syntax as $syntax) { - $syntax->setup($parse); + $syntax->setup($parse, $emit); + } + + foreach (Transformations::registered() as $kind => $function) { + $emit->transform($kind, $function); } - $out= new MemoryOutputStream(); - $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); $emit->emit($parse->execute()); // var_dump($out->getBytes()); self::$cl->setClassBytes($name, $out->getBytes()); From 77b1337810ab18c66e26f41d807c525eb352eadc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 13:39:08 +0200 Subject: [PATCH 042/926] Add test for compiler transformations --- .../emit/TransformationsTest.class.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php new file mode 100755 index 00000000..eaf8976d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -0,0 +1,40 @@ +value->annotation('getters')) { + foreach ($class->value->properties() as $property) { + $class->value->inject(new Method( + ['public'], + $property->name, + new Signature([], $property->type), + [new Code('return $this->'.$property->name.';')] + )); + } + } + yield $class; + }); + } + + #[@test] + public function generates_accessor() { + $t= $this->type('<> class { + private int $id; + private string $name; + + public function __construct(int $id, string $name) { + $this->id= $id; + $this->name= $name; + } + }'); + $this->assertTrue($t->hasMethod('id')); + } +} \ No newline at end of file From 320e5fb7211ca221a5a4aba5fcce5c02aabc8dbe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:03:20 +0200 Subject: [PATCH 043/926] Include syntaxes in usage output --- .../php/xp/compiler/CompileRunner.class.php | 14 ++------ src/main/php/xp/compiler/Usage.class.php | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100755 src/main/php/xp/compiler/Usage.class.php diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 7928d388..4932aee2 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -7,7 +7,6 @@ use lang\ast\Parse; use lang\ast\Tokens; use lang\ast\transform\Transformations; -use lang\reflect\Package; use text\StreamTokenizer; use util\cmd\Console; use util\profiling\Timer; @@ -44,16 +43,9 @@ class CompileRunner { /** @return int */ public static function main(array $args) { - if (empty($args)) { - Console::$err->writeLine('Usage: xp compile []'); - return 2; - } - - $syntaxes= []; - foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - $syntaxes[]= $class->newInstance(); - } + if (empty($args)) return Usage::main($args); + $syntaxes= Package::forName('lang.ast.syntax')->getClasses(); $target= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; $in= $out= '-'; for ($i= 0; $i < sizeof($args); $i++) { @@ -88,7 +80,7 @@ public static function main(array $args) { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); foreach ($syntaxes as $syntax) { - $syntax->setup($parse, $emitter); + $syntax->newInstance()->setup($parse, $emitter); } foreach (Transformations::registered() as $kind => $function) { diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php new file mode 100755 index 00000000..47e7e5ca --- /dev/null +++ b/src/main/php/xp/compiler/Usage.class.php @@ -0,0 +1,33 @@ +writeLine('Usage: xp compile []'); + Console::$err->writeLine(); + + // Show syntax implementations sorted by class loader + $loaders= $sorted= []; + foreach (Package::forName('lang.ast.syntax')->getClasses() as $syntax) { + $l= $syntax->getClassLoader(); + $hash= $l->hashCode(); + if (isset($sorted[$hash])) { + $sorted[$hash][]= $syntax; + } else { + $loaders[$hash]= $l; + $sorted[$hash]= [$syntax]; + } + } + foreach ($sorted as $hash => $list) { + Console::$err->writeLine("\033[33m@", $loaders[$hash], "\033[0m"); + foreach ($list as $syntax) { + Console::$err->writeLine($syntax->getName()); + } + } + return 2; + } +} \ No newline at end of file From c05bafc7c0d4265557ffc60d44b07cb5bad5bf28 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:30:04 +0200 Subject: [PATCH 044/926] Extract transformations to syntax package --- src/main/php/lang/ast/Compiled.class.php | 43 ++++++++++--------- .../lang/ast/CompilingClassloader.class.php | 26 +---------- .../ast/syntax/TransformationApi.class.php | 12 ++++++ .../php/xp/compiler/CompileRunner.class.php | 4 -- .../ast/unittest/emit/EmittingTest.class.php | 4 -- 5 files changed, 37 insertions(+), 52 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/TransformationApi.class.php diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index c1bf9a6d..e420c987 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -1,15 +1,32 @@ getResourceAsStream($file); + return self::parse($version, $stream->in(), new self(), $file)->compiled; + } + + private static function parse($version, $in, $out, $file) { + try { + $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); + $emitter= self::$emit[$version]->newInstance($out); + foreach (self::$syntax as $syntax) { + $syntax->setup($parse, $emitter); + } + $emitter->emit($parse->execute()); + return $out; + } finally { + $in->close(); + } + } + /** * Opens path * @@ -21,23 +38,9 @@ class Compiled implements OutputStream { public function stream_open($path, $mode, $options, &$opened) { list($version, $file)= explode('://', $path); $stream= self::$source[$file]->getResourceAsStream($file); - $in= $stream->in(); - - try { - $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); - $emitter= self::$emit[$version]->newInstance($this); - foreach (Transformations::registered() as $kind => $function) { - $emitter->transform($kind, $function); - } - $emitter->emit($parse->execute()); - $opened= $stream->getURI(); - return true; - } catch (Error $e) { - $message= sprintf('Syntax error in %s, line %d: %s', $e->getFile(), $e->getLine(), $e->getMessage()); - throw new ClassFormatException($message); - } finally { - $in->close(); - } + self::parse($version, $stream->in(), $this, $file); + $opened= $stream->getURI(); + return true; } /** @param string $bytes */ diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index a4c0127b..e2af241a 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -21,6 +21,7 @@ private function __construct($emit) { $this->version= $emit->getSimpleName(); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { $this->syntax[]= $class->newInstance(); + Compiled::$syntax[]= $class->newInstance(); } Compiled::$emit[$this->version]= $emit; @@ -203,30 +204,7 @@ public function loadClassBytes($class) { throw new ClassNotFoundException($class); } - $declaration= new MemoryOutputStream(); - $file= strtr($class, '.', '/').self::EXTENSION; - $in= $source->getResourceAsStream($file)->in(); - - try { - $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); - $emitter= $this->emit->newInstance($declaration); - - foreach ($this->syntax as $syntax) { - $syntax->setup($parse, $emitter); - } - - foreach (Transformations::registered() as $kind => $function) { - $emitter->transform($kind, $function); - } - $emitter->emit($parse->execute()); - - return $declaration->getBytes(); - } catch (Errors $e) { - $message= sprintf('Syntax error in %s, line %d: %s', $e->getFile(), $e->getLine(), $e->getMessage()); - throw new ClassFormatException($message); - } finally { - $in->close(); - } + return Compiled::bytes($this->version, $source, strtr($class, '.', '/').self::EXTENSION); } /** diff --git a/src/main/php/lang/ast/syntax/TransformationApi.class.php b/src/main/php/lang/ast/syntax/TransformationApi.class.php new file mode 100755 index 00000000..c22073c0 --- /dev/null +++ b/src/main/php/lang/ast/syntax/TransformationApi.class.php @@ -0,0 +1,12 @@ + $function) { + $emitter->transform($kind, $function); + } + } +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 4932aee2..9f40e331 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -6,7 +6,6 @@ use lang\ast\Errors; use lang\ast\Parse; use lang\ast\Tokens; -use lang\ast\transform\Transformations; use text\StreamTokenizer; use util\cmd\Console; use util\profiling\Timer; @@ -83,9 +82,6 @@ public static function main(array $args) { $syntax->newInstance()->setup($parse, $emitter); } - foreach (Transformations::registered() as $kind => $function) { - $emitter->transform($kind, $function); - } $emitter->emit($parse->execute()); $t->stop(); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index be5b1627..0ba6235e 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -40,10 +40,6 @@ protected function type($code) { $syntax->setup($parse, $emit); } - foreach (Transformations::registered() as $kind => $function) { - $emit->transform($kind, $function); - } - $emit->emit($parse->execute()); // var_dump($out->getBytes()); self::$cl->setClassBytes($name, $out->getBytes()); From de3734bd7f4ed7ac94e609c9b1f2027d14543b73 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:33:43 +0200 Subject: [PATCH 045/926] Remove unused cod --- src/main/php/lang/ast/CompilingClassloader.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index e2af241a..724ae2e2 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -13,14 +13,12 @@ class CompilingClassLoader implements IClassLoader { const EXTENSION = '.php'; private static $instance= []; - private $syntax= []; private $version; /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - $this->syntax[]= $class->newInstance(); Compiled::$syntax[]= $class->newInstance(); } From 54dcfe25ca94f11aeb8d9c3072cd92ce7314f408 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:41:02 +0200 Subject: [PATCH 046/926] Extract null-safe instance operator --- src/main/php/lang/ast/Emitter.class.php | 15 -------- src/main/php/lang/ast/Parse.class.php | 35 +++++------------- .../php/lang/ast/syntax/NullSafe.class.php | 37 +++++++++++++++++++ 3 files changed, 47 insertions(+), 40 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/NullSafe.class.php diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index a3b8d791..7a52e30c 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -830,21 +830,6 @@ protected function emitInstance($instance) { } } - protected function emitNullSafeInstance($instance) { - $t= $this->temp(); - $this->out->write('null === ('.$t.'= '); - $this->emit($instance->expression); - $this->out->write(') ? null : '.$t.'->'); - - if ('name' === $instance->member->kind) { - $this->out->write($instance->member->value); - } else { - $this->out->write('{'); - $this->emit($instance->member); - $this->out->write('}'); - } - } - protected function emitUnpack($unpack) { $this->out->write('...'); $this->emit($unpack); diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 961a5deb..738aa8d8 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -137,21 +137,6 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->infix('?->', 80, function($node, $left) { - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $expr= $this->expression(0); - $this->token= $this->next('}'); - } else { - $expr= $this->token; - $this->token= $this->advance(); - } - - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'nullsafeinstance'; - return $node; - }); - $this->infix('::', 80, function($node, $left) { $node->value= new ScopeExpression($this->scope->resolve($left->value), $this->token); $node->kind= 'scope'; @@ -1437,7 +1422,7 @@ private static function symbol($id, $lbp= 0) { return $symbol; } - private static function constant($id, $value) { + public static function constant($id, $value) { $const= self::symbol($id); $const->nud= function($node) use($value) { $node->kind= 'literal'; @@ -1447,7 +1432,7 @@ private static function constant($id, $value) { return $const; } - private function assignment($id) { + public function assignment($id) { $infix= self::symbol($id, 10); $infix->led= function($node, $left) use($id) { $node->kind= 'assignment'; @@ -1457,9 +1442,9 @@ private function assignment($id) { return $infix; } - private function infix($id, $bp, $led= null) { + public function infix($id, $bp, $led= null) { $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($node, $left) use($id, $bp) { + $infix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id, $bp) { $node->value= new BinaryExpression($left, $id, $this->expression($bp)); $node->kind= 'binary'; return $node; @@ -1467,9 +1452,9 @@ private function infix($id, $bp, $led= null) { return $infix; } - private function infixr($id, $bp, $led= null) { + public function infixr($id, $bp, $led= null) { $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($node, $left) use($id, $bp) { + $infix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id, $bp) { $node->value= new BinaryExpression($left, $id, $this->expression($bp - 1)); $node->kind= 'binary'; return $node; @@ -1477,7 +1462,7 @@ private function infixr($id, $bp, $led= null) { return $infix; } - private function infixt($id, $bp) { + public function infixt($id, $bp) { $infix= self::symbol($id, $bp); $infix->led= function($node, $left) use($id, $bp) { $node->value= new BinaryExpression($left, $id, $this->expressionWithThrows($bp - 1)); @@ -1487,7 +1472,7 @@ private function infixt($id, $bp) { return $infix; } - private function prefix($id, $nud= null) { + public function prefix($id, $nud= null) { $prefix= self::symbol($id); $prefix->nud= $nud ?: function($node) use($id) { $node->value= new UnaryExpression($this->expression(70), $id); @@ -1497,9 +1482,9 @@ private function prefix($id, $nud= null) { return $prefix; } - private function suffix($id, $bp, $led= null) { + public function suffix($id, $bp, $led= null) { $suffix= self::symbol($id, $bp); - $suffix->led= $led ?: function($node, $left) use($id) { + $suffix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id) { $node->value= new UnaryExpression($left, $id); $node->kind= 'unary'; return $node; diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php new file mode 100755 index 00000000..f527186c --- /dev/null +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -0,0 +1,37 @@ +infix('?->', 80, function($node, $left) { + if ('{' === $this->token->value) { + $this->token= $this->advance(); + $expr= $this->expression(0); + $this->token= $this->next('}'); + } else { + $expr= $this->token; + $this->token= $this->advance(); + } + + $node->value= new InstanceExpression($left, $expr); + $node->kind= 'nullsafeinstance'; + return $node; + }); + $emitter->handle('nullsafeinstance', function($instance) { + $t= $this->temp(); + $this->out->write('null === ('.$t.'= '); + $this->emit($instance->value->expression); + $this->out->write(') ? null : '.$t.'->'); + + if ('name' === $instance->value->member->kind) { + $this->out->write($instance->value->member->value); + } else { + $this->out->write('{'); + $this->emit($instance->value->member); + $this->out->write('}'); + } + }); + } +} \ No newline at end of file From 5714affe838efbd52015e6da365e2ba7ddc82074 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:49:34 +0200 Subject: [PATCH 047/926] Ensure syntaxes are only discovered once --- src/main/php/lang/ast/CompilingClassloader.class.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 724ae2e2..8fa9ba44 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -15,13 +15,15 @@ class CompilingClassLoader implements IClassLoader { private static $instance= []; private $version; - /** Creates a new instances with a given PHP runtime */ - private function __construct($emit) { - $this->version= $emit->getSimpleName(); + static function __static() { foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { Compiled::$syntax[]= $class->newInstance(); } + } + /** Creates a new instances with a given PHP runtime */ + private function __construct($emit) { + $this->version= $emit->getSimpleName(); Compiled::$emit[$this->version]= $emit; stream_wrapper_register($this->version, Compiled::class); } From 6c37081a655fe94eefe0c839ea61e91cb2fe7e5b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:52:40 +0200 Subject: [PATCH 048/926] Use Compiled::$syntax enumeration --- src/main/php/xp/compiler/Usage.class.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 47e7e5ca..00c8f69b 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,6 +1,6 @@ getClasses() as $syntax) { - $l= $syntax->getClassLoader(); + foreach (Compiled::$syntax as $syntax) { + $t= typeof($syntax); + $l= $t->getClassLoader(); $hash= $l->hashCode(); if (isset($sorted[$hash])) { - $sorted[$hash][]= $syntax; + $sorted[$hash][]= $t; } else { $loaders[$hash]= $l; - $sorted[$hash]= [$syntax]; + $sorted[$hash]= [$t]; } } foreach ($sorted as $hash => $list) { From 594ce22b7612ead3a59fb88720edc57fdd3937d8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:55:33 +0200 Subject: [PATCH 049/926] Use Compiled::$syntax enumeration in XP subcommand --- src/main/php/xp/compiler/CompileRunner.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 9f40e331..468eb52b 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -2,6 +2,7 @@ use io\Path; use lang\Runtime; +use lang\ast\Compiled; use lang\ast\Emitter; use lang\ast\Errors; use lang\ast\Parse; @@ -44,7 +45,6 @@ class CompileRunner { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $syntaxes= Package::forName('lang.ast.syntax')->getClasses(); $target= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; $in= $out= '-'; for ($i= 0; $i < sizeof($args); $i++) { @@ -78,8 +78,8 @@ public static function main(array $args) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); - foreach ($syntaxes as $syntax) { - $syntax->newInstance()->setup($parse, $emitter); + foreach (Compiled::$syntax as $syntax) { + $syntax->setup($parse, $emitter); } $emitter->emit($parse->execute()); From 0bd05a4a557dbc82967cd9944c9eeb7fcf91b4c5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 14:57:41 +0200 Subject: [PATCH 050/926] Move syntax enumeration initialization to module.xp --- src/main/php/lang/ast/CompilingClassloader.class.php | 6 ------ src/main/php/module.xp | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 8fa9ba44..91fca9e8 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -15,12 +15,6 @@ class CompilingClassLoader implements IClassLoader { private static $instance= []; private $version; - static function __static() { - foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - Compiled::$syntax[]= $class->newInstance(); - } - } - /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 798880e2..da5f2126 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -1,6 +1,7 @@ getClasses() as $class) { + Compiled::$syntax[]= $class->newInstance(); + } + if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); } From 9b20a0da7b5606ceb0f2500ea18d39d0725f5254 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 15:01:15 +0200 Subject: [PATCH 051/926] Move syntaxes to CompilingClassloader --- src/main/php/lang/ast/Compiled.class.php | 4 ++-- src/main/php/lang/ast/CompilingClassloader.class.php | 1 + src/main/php/module.xp | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 4 ++-- src/main/php/xp/compiler/Usage.class.php | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index e420c987..affa1c5c 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -4,7 +4,7 @@ use text\StreamTokenizer; class Compiled implements OutputStream { - public static $source= [], $syntax= [], $emit= []; + public static $source= [], $emit= []; private $compiled= '', $offset= 0; @@ -17,7 +17,7 @@ private static function parse($version, $in, $out, $file) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= self::$emit[$version]->newInstance($out); - foreach (self::$syntax as $syntax) { + foreach (CompilingClassloader::$syntax as $syntax) { $syntax->setup($parse, $emitter); } $emitter->emit($parse->execute()); diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 91fca9e8..62ba7be0 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -13,6 +13,7 @@ class CompilingClassLoader implements IClassLoader { const EXTENSION = '.php'; private static $instance= []; + public static $syntax= []; private $version; /** Creates a new instances with a given PHP runtime */ diff --git a/src/main/php/module.xp b/src/main/php/module.xp index da5f2126..da9384b2 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -14,7 +14,7 @@ module xp-framework/compiler { ClassLoader::registerLoader(CompilingClassloader::instanceFor($runtime)); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - Compiled::$syntax[]= $class->newInstance(); + CompilingClassloader::$syntax[]= $class->newInstance(); } if (!interface_exists(\IDisposable::class, false)) { diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 468eb52b..bde069ba 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -2,7 +2,7 @@ use io\Path; use lang\Runtime; -use lang\ast\Compiled; +use lang\ast\CompilingClassloader; use lang\ast\Emitter; use lang\ast\Errors; use lang\ast\Parse; @@ -78,7 +78,7 @@ public static function main(array $args) { try { $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); - foreach (Compiled::$syntax as $syntax) { + foreach (CompilingClassloader::$syntax as $syntax) { $syntax->setup($parse, $emitter); } diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 00c8f69b..83a395d5 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,6 +1,6 @@ getClassLoader(); $hash= $l->hashCode(); From 26b879d906d789362945f30c60b8e4941c24a59f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 15:07:23 +0200 Subject: [PATCH 052/926] Use CompilingClassLoader::$syntax enumeration from test suite --- .../php/lang/ast/unittest/emit/EmittingTest.class.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 0ba6235e..3ab5b8e4 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -3,25 +3,20 @@ use io\streams\MemoryOutputStream; use io\streams\StringWriter; use lang\DynamicClassLoader; +use lang\ast\CompilingClassLoader; use lang\ast\Emitter; use lang\ast\Node; use lang\ast\Parse; use lang\ast\Tokens; -use lang\ast\transform\Transformations; -use lang\reflect\Package; use text\StringTokenizer; use unittest\TestCase; abstract class EmittingTest extends TestCase { private static $cl; - private static $syntax= []; private static $id= 0; static function __static() { self::$cl= DynamicClassLoader::instanceFor(self::class); - foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { - self::$syntax[]= $class->newInstance(); - } } /** @@ -36,7 +31,7 @@ protected function type($code) { $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); - foreach (self::$syntax as $syntax) { + foreach (CompilingClassLoader::$syntax as $syntax) { $syntax->setup($parse, $emit); } From e09a5511aedf8ef3c555e751cd9dd01e3f9655e7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 17:41:48 +0200 Subject: [PATCH 053/926] Refactor parser, extracting rules into static initializer --- src/main/php/lang/ast/Parse.class.php | 1613 +++++++++-------- .../lang/ast/syntax/CompactMethods.class.php | 30 +- .../php/lang/ast/syntax/NullSafe.class.php | 14 +- src/main/php/lang/ast/syntax/Using.class.php | 16 +- .../ast/unittest/parse/ErrorsTest.class.php | 2 +- 5 files changed, 867 insertions(+), 808 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 738aa8d8..8524b63d 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -39,13 +39,15 @@ class Parse { private static $symbols= []; + private static $body= []; - private $tokens, $file, $token, $scope; - private $comment= null; - private $body= []; - private $queue= []; + private $tokens, $file; private $errors= []; + public $token, $scope; + public $comment= null; + public $queue= []; + static function __static() { self::symbol(':'); self::symbol(';'); @@ -64,72 +66,59 @@ static function __static() { self::constant('true', 'true'); self::constant('false', 'false'); self::constant('null', 'null'); - } - - /** - * Creates a new parse instance - * - * @param lang.ast.Tokens $tokens - * @param string $file - * @param lang.ast.Scope $scope - */ - public function __construct($tokens, $file= null, $scope= null) { - $this->tokens= $tokens->getIterator(); - $this->scope= $scope ?: new Scope(null); - $this->file= $file; - // Setup parse rules - $this->infixt('??', 30); - $this->infixt('?:', 30); - - $this->infixr('&&', 30); - $this->infixr('||', 30); - - $this->infixr('==', 40); - $this->infixr('===', 40); - $this->infixr('!=', 40); - $this->infixr('!==', 40); - $this->infixr('<', 40); - $this->infixr('<=', 40); - $this->infixr('>', 40); - $this->infixr('>=', 40); - $this->infixr('<=>', 40); - - $this->infix('+', 50); - $this->infix('-', 50); - $this->infix('&', 50); - $this->infix('|', 50); - $this->infix('^', 50); - - $this->infix('*', 60); - $this->infix('/', 60); - $this->infix('%', 60); - $this->infix('.', 60); - $this->infix('**', 60); - - $this->infixr('<<', 70); - $this->infixr('>>', 70); - - $this->infix('instanceof', 60, function($node, $left) { - if ('name' === $this->token->kind) { - $node->value= new InstanceOfExpression($left, $this->scope->resolve($this->token->value)); - $this->token= $this->advance(); + self::infixt('??', 30); + self::infixt('?:', 30); + self::infixr('&&', 30); + self::infixr('||', 30); + + self::infixr('==', 40); + self::infixr('===', 40); + self::infixr('!=', 40); + self::infixr('!==', 40); + self::infixr('<', 40); + self::infixr('<=', 40); + self::infixr('>', 40); + self::infixr('>=', 40); + self::infixr('<=>', 40); + + self::infix('+', 50); + self::infix('-', 50); + self::infix('&', 50); + self::infix('|', 50); + self::infix('^', 50); + self::suffix('++', 50); + self::suffix('--', 50); + + self::infix('*', 60); + self::infix('/', 60); + self::infix('%', 60); + self::infix('.', 60); + self::infix('**', 60); + + self::infixr('<<', 70); + self::infixr('>>', 70); + + self::infix('instanceof', 60, function($parse, $node, $left) { + if ('name' === $parse->token->kind) { + $node->value= new InstanceOfExpression($left, $parse->scope->resolve($parse->token->value)); + $parse->forward(); } else { - $node->value= new InstanceOfExpression($left, $this->expression(0)); + $node->value= new InstanceOfExpression($left, $parse->expression(0)); } $node->kind= 'instanceof'; return $node; }); - $this->infix('->', 80, function($node, $left) { - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $expr= $this->expression(0); - $this->token= $this->next('}'); + self::infix('->', 80, function($parse, $node, $left) { + if ('{' === $parse->token->value) { + $parse->forward(); + $expr= $parse->expression(0); + $parse->expecting('}', 'dynamic member'); } else { - $expr= $this->token; - $this->token= $this->advance(); + $expr= $parse->token; + $parse->forward(); } $node->value= new InstanceExpression($left, $expr); @@ -137,23 +126,23 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->infix('::', 80, function($node, $left) { - $node->value= new ScopeExpression($this->scope->resolve($left->value), $this->token); + self::infix('::', 80, function($parse, $node, $left) { + $node->value= new ScopeExpression($parse->scope->resolve($left->value), $parse->token); $node->kind= 'scope'; - $this->token= $this->advance(); + $parse->forward(); return $node; }); - $this->infix('==>', 80, function($node, $left) { - $this->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + self::infix('==>', 80, function($parse, $node, $left) { + $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); $signature= new Signature([new Parameter($left->value, null)], null); - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $statements= $this->statements(); - $this->token= $this->next('}'); + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $parse->statements(); + $parse->expecting('}', 'arrow function'); } else { - $statements= $this->expressionWithThrows(0); + $statements= $parse->expressionWithThrows(0); } $node->value= new LambdaExpression($signature, $statements); @@ -161,21 +150,21 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->infix('(', 80, function($node, $left) { - $arguments= $this->arguments(); - $this->token= $this->next(')'); + self::infix('(', 80, function($parse, $node, $left) { + $arguments= $parse->expressions(); + $parse->expecting(')', 'invoke expression'); $node->value= new InvokeExpression($left, $arguments); $node->kind= 'invoke'; return $node; }); - $this->infix('[', 80, function($node, $left) { - if (']' === $this->token->value) { + self::infix('[', 80, function($parse, $node, $left) { + if (']' === $parse->token->value) { $expr= null; - $this->token= $this->advance(); + $parse->forward(); } else { - $expr= $this->expression(0); - $this->token= $this->next(']'); + $expr= $parse->expression(0); + $parse->expecting(']', 'offset access'); } $node->value= new OffsetExpression($left, $expr); @@ -183,50 +172,47 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->infix('{', 80, function($node, $left) { - $expr= $this->expression(0); - $this->token= $this->next('}'); + self::infix('{', 80, function($parse, $node, $left) { + $expr= $parse->expression(0); + $parse->token= $parse->next('}'); $node->value= new OffsetExpression($left, $expr); $node->kind= 'offset'; return $node; }); - $this->infix('?', 80, function($node, $left) { - $when= $this->expressionWithThrows(0); - $this->token= $this->next(':'); - $else= $this->expressionWithThrows(0); + self::infix('?', 80, function($parse, $node, $left) { + $when= $parse->expressionWithThrows(0); + $parse->token= $parse->next(':'); + $else= $parse->expressionWithThrows(0); $node->value= new TernaryExpression($left, $when, $else); $node->kind= 'ternary'; return $node; }); - $this->suffix('++', 50); - $this->suffix('--', 50); - - $this->prefix('@'); - $this->prefix('&'); - $this->prefix('~'); - $this->prefix('!'); - $this->prefix('+'); - $this->prefix('-'); - $this->prefix('++'); - $this->prefix('--'); - $this->prefix('clone'); - - $this->assignment('='); - $this->assignment('&='); - $this->assignment('|='); - $this->assignment('^='); - $this->assignment('+='); - $this->assignment('-='); - $this->assignment('*='); - $this->assignment('/='); - $this->assignment('.='); - $this->assignment('**='); - $this->assignment('>>='); - $this->assignment('<<='); - $this->assignment('??='); + self::prefix('@'); + self::prefix('&'); + self::prefix('~'); + self::prefix('!'); + self::prefix('+'); + self::prefix('-'); + self::prefix('++'); + self::prefix('--'); + self::prefix('clone'); + + self::assignment('='); + self::assignment('&='); + self::assignment('|='); + self::assignment('^='); + self::assignment('+='); + self::assignment('-='); + self::assignment('*='); + self::assignment('/='); + self::assignment('.='); + self::assignment('**='); + self::assignment('>>='); + self::assignment('<<='); + self::assignment('??='); // This is ambiguous: // @@ -235,7 +221,7 @@ public function __construct($tokens, $file= null, $scope= null) { // - A cast `(int)$a` or `(int)($a / 2)`. // // Resolve by looking ahead after the closing ")" - $this->prefix('(', function($node) { + self::prefix('(', function($parse, $node) { static $types= [ '<' => true, '>' => true, @@ -244,118 +230,118 @@ public function __construct($tokens, $file= null, $scope= null) { ':' => true ]; - $skipped= [$node, $this->token]; + $skipped= [$node, $parse->token]; $cast= true; $level= 1; - while ($level > 0 && null !== $this->token->value) { - if ('(' === $this->token->value) { + while ($level > 0 && null !== $parse->token->value) { + if ('(' === $parse->token->value) { $level++; - } else if (')' === $this->token->value) { + } else if (')' === $parse->token->value) { $level--; - } else if ('name' !== $this->token->kind && !isset($types[$this->token->value])) { + } else if ('name' !== $parse->token->kind && !isset($types[$parse->token->value])) { $cast= false; } - $this->token= $this->advance(); - $skipped[]= $this->token; + $parse->forward(); + $skipped[]= $parse->token; } - $this->queue= array_merge($skipped, $this->queue); + $parse->queue= array_merge($skipped, $parse->queue); - if (':' === $this->token->value || '==>' === $this->token->value) { - $this->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + if (':' === $parse->token->value || '==>' === $parse->token->value) { + $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); $node->kind= 'lambda'; - $this->token= $this->advance(); - $signature= $this->signature(); - $this->token= $this->advance(); - - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $statements= $this->statements(); - $this->token= $this->next('}'); + $parse->forward(); + $signature= $parse->signature(); + $parse->forward(); + + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $parse->statements(); + $parse->expecting('}', 'arrow function'); } else { - $statements= $this->expressionWithThrows(0); + $statements= $parse->expressionWithThrows(0); } $node->value= new LambdaExpression($signature, $statements); - } else if ($cast && ('operator' !== $this->token->kind || '(' === $this->token->value || '[' === $this->token->value)) { + } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { $node->kind= 'cast'; - $this->token= $this->advance(); - $this->token= $this->expect('('); - $type= $this->type0(false); - $this->token= $this->expect(')'); - $node->value= new CastExpression($type, $this->expression(0)); + $parse->forward(); + $parse->expecting('(', 'cast'); + $type= $parse->type0($parse, false); + $parse->expecting(')', 'cast'); + $node->value= new CastExpression($type, $parse->expression(0)); } else { $node->kind= 'braced'; - $this->token= $this->advance(); - $this->token= $this->expect('('); - $node->value= $this->expression(0); - $this->token= $this->expect(')'); + $parse->forward(); + $parse->expecting('(', 'braced'); + $node->value= $parse->expression(0); + $parse->expecting(')', 'braced'); } return $node; }); - $this->prefix('[', function($node) { + self::prefix('[', function($parse, $node) { $values= []; - while (']' !== $this->token->value) { - $expr= $this->expression(0); + while (']' !== $parse->token->value) { + $expr= $parse->expression(0); - if ('=>' === $this->token->value) { - $this->token= $this->advance(); - $values[]= [$expr, $this->expression(0)]; + if ('=>' === $parse->token->value) { + $parse->forward(); + $values[]= [$expr, $parse->expression(0)]; } else { $values[]= [null, $expr]; } - if (']' === $this->token->value) { + if (']' === $parse->token->value) { break; } else { - $this->token= $this->next(',', 'array literal'); + $parse->expecting(',', 'array literal'); } } - $this->token= $this->next(']', 'array literal'); + $parse->expecting(']', 'array literal'); $node->kind= 'array'; $node->value= $values; return $node; }); - $this->prefix('new', function($node) { - $type= $this->token; - $this->token= $this->advance(); + self::prefix('new', function($parse, $node) { + $type= $parse->token; + $parse->forward(); - $this->token= $this->expect('('); - $arguments= $this->arguments(); - $this->token= $this->expect(')'); + $parse->expecting('(', 'new arguments'); + $arguments= $parse->expressions(); + $parse->expecting(')', 'new arguments'); if ('variable' === $type->kind) { $node->value= new NewExpression('$'.$type->value, $arguments); $node->kind= 'new'; } else if ('class' === $type->value) { - $node->value= new NewClassExpression($this->clazz(null), $arguments); + $node->value= new NewClassExpression($parse->clazz(null), $arguments); $node->kind= 'newclass'; } else { - $node->value= new NewExpression($this->scope->resolve($type->value), $arguments); + $node->value= new NewExpression($parse->scope->resolve($type->value), $arguments); $node->kind= 'new'; } return $node; }); - $this->prefix('yield', function($node) { - if (';' === $this->token->value) { + self::prefix('yield', function($parse, $node) { + if (';' === $parse->token->value) { $node->kind= 'yield'; $node->value= new YieldExpression(null, null); - } else if ('from' === $this->token->value) { - $this->token= $this->advance(); + } else if ('from' === $parse->token->value) { + $parse->forward(); $node->kind= 'from'; - $node->value= $this->expression(0); + $node->value= $parse->expression(0); } else { $node->kind= 'yield'; - $expr= $this->expression(0); - if ('=>' === $this->token->value) { - $this->token= $this->advance(); - $node->value= new YieldExpression($expr, $this->expression(0)); + $expr= $parse->expression(0); + if ('=>' === $parse->token->value) { + $parse->forward(); + $node->value= new YieldExpression($expr, $parse->expression(0)); } else { $node->value= new YieldExpression(null, $expr); } @@ -363,23 +349,23 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->prefix('...', function($node) { + self::prefix('...', function($parse, $node) { $node->kind= 'unpack'; - $node->value= $this->expression(0); + $node->value= $parse->expression(0); return $node; }); - $this->prefix('fn', function($node) { - $signature= $this->signature(); + self::prefix('fn', function($parse, $node) { + $signature= $parse->signature(); - $this->token= $this->expect('=>'); + $parse->token= $parse->expect('=>'); - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); + if ('{' === $parse->token->value) { + $parse->token= $parse->expect('{'); + $statements= $parse->statements(); + $parse->token= $parse->expect('}'); } else { - $statements= $this->expressionWithThrows(0); + $statements= $parse->expressionWithThrows(0); } $node->value= new LambdaExpression($signature, $statements); @@ -387,203 +373,204 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->prefix('function', function($node) { + + self::prefix('function', function($parse, $node) { // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; // the latter explicitely becomes a statement by pushing a semicolon. - if ('(' === $this->token->value) { + if ('(' === $parse->token->value) { $node->kind= 'closure'; - $signature= $this->signature(); + $signature= $parse->signature(); - if ('use' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->advance(); + if ('use' === $parse->token->value) { + $parse->forward(); + $parse->forward(); $use= []; - while (')' !== $this->token->value) { - if ('&' === $this->token->value) { - $this->token= $this->advance(); - $use[]= '&$'.$this->token->value; + while (')' !== $parse->token->value) { + if ('&' === $parse->token->value) { + $parse->forward(); + $use[]= '&$'.$parse->token->value; } else { - $use[]= '$'.$this->token->value; + $use[]= '$'.$parse->token->value; } - $this->token= $this->advance(); - if (')' === $this->token->value) break; - $this->token= $this->expect(',', 'use list'); + $parse->forward(); + if (')' === $parse->token->value) break; + $parse->token= $parse->expect(',', 'use list'); } - $this->token= $this->expect(')'); + $parse->token= $parse->expect(')'); } else { $use= null; } - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); + $parse->token= $parse->expect('{'); + $statements= $parse->statements(); + $parse->token= $parse->expect('}'); $node->value= new ClosureExpression($signature, $use, $statements); } else { $node->kind= 'function'; - $name= $this->token->value; - $this->token= $this->advance(); - $signature= $this->signature(); - - if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - $n= new Node($this->token->symbol); - $this->token= $this->advance(); - $n->value= $this->expressionWithThrows(0); - $n->line= $this->token->line; + $name= $parse->token->value; + $parse->forward(); + $signature= $parse->signature(); + + if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' + $n= new Node($parse->token->symbol); + $parse->forward(); + $n->value= $parse->expressionWithThrows(0); + $n->line= $parse->token->line; $n->kind= 'return'; $statements= [$n]; - $this->token= $this->next(';'); + $parse->token= $parse->next(';'); } else { // Regular function - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); + $parse->token= $parse->expect('{'); + $statements= $parse->statements(); + $parse->token= $parse->expect('}'); } - $this->queue= [$this->token]; - $this->token= new Node(self::symbol(';')); + $parse->queue= [$parse->token]; + $parse->token= new Node(self::symbol(';')); $node->value= new FunctionDeclaration($name, $signature, $statements); } return $node; }); - $this->prefix('static', function($node) { - if ('variable' === $this->token->kind) { + self::prefix('static', function($parse, $node) { + if ('variable' === $parse->token->kind) { $node->kind= 'static'; $node->value= []; - while (';' !== $this->token->value) { - $variable= $this->token->value; - $this->token= $this->advance(); + while (';' !== $parse->token->value) { + $variable= $parse->token->value; + $parse->forward(); - if ('=' === $this->token->value) { - $this->token= $this->advance(); - $initial= $this->expression(0); + if ('=' === $parse->token->value) { + $parse->forward(); + $initial= $parse->expression(0); } else { $initial= null; } $node->value[$variable]= $initial; - if (',' === $this->token->value) { - $this->token= $this->advance(); + if (',' === $parse->token->value) { + $parse->forward(); } } } return $node; }); - $this->prefix('goto', function($node) { + self::prefix('goto', function($parse, $node) { $node->kind= 'goto'; - $node->value= $this->token->value; - $this->token= $this->advance(); + $node->value= $parse->token->value; + $parse->forward(); return $node; }); - $this->prefix('(name)', function($node) { - if (':' === $this->token->value) { + self::prefix('(name)', function($parse, $node) { + if (':' === $parse->token->value) { $node->kind= 'label'; - $this->token= new Node(self::symbol(';')); + $parse->token= new Node(self::symbol(';')); } return $node; }); - $this->stmt('kind= 'start'; - $node->value= $this->token->value; + $node->value= $parse->token->value; - $this->token= $this->advance(); + $parse->forward(); return $node; }); - $this->stmt('{', function($node) { + self::stmt('{', function($parse, $node) { $node->kind= 'block'; - $node->value= $this->statements(); - $this->token= $this->advance(); + $node->value= $parse->statements(); + $parse->forward(); return $node; }); - $this->prefix('echo', function($node) { + self::prefix('echo', function($parse, $node) { $node->kind= 'echo'; - $node->value= $this->arguments(';'); + $node->value= $parse->expressions(';'); return $node; }); - $this->stmt('namespace', function($node) { + self::stmt('namespace', function($parse, $node) { $node->kind= 'package'; - $node->value= $this->token->value; + $node->value= $parse->token->value; - $this->token= $this->advance(); - $this->token= $this->expect(';'); + $parse->forward(); + $parse->expecting(';', 'namespace'); - $this->scope->package($node->value); + $parse->scope->package($node->value); return $node; }); - $this->stmt('use', function($node) { - if ('function' === $this->token->value) { + self::stmt('use', function($parse, $node) { + if ('function' === $parse->token->value) { $node->kind= 'importfunction'; - $this->token= $this->advance(); - } else if ('const' === $this->token->value) { + $parse->forward(); + } else if ('const' === $parse->token->value) { $node->kind= 'importconst'; - $this->token= $this->advance(); + $parse->forward(); } else { $node->kind= 'import'; } - $import= $this->token->value; - $this->token= $this->advance(); + $import= $parse->token->value; + $parse->forward(); - if ('{' === $this->token->value) { + if ('{' === $parse->token->value) { $types= []; - $this->token= $this->advance(); - while ('}' !== $this->token->value) { - $class= $import.$this->token->value; - - $this->token= $this->advance(); - if ('as' === $this->token->value) { - $this->token= $this->advance(); - $types[$class]= $this->token->value; - $this->scope->import($this->token->value); - $this->token= $this->advance(); + $parse->forward(); + while ('}' !== $parse->token->value) { + $class= $import.$parse->token->value; + + $parse->forward(); + if ('as' === $parse->token->value) { + $parse->forward(); + $types[$class]= $parse->token->value; + $parse->scope->import($parse->token->value); + $parse->forward(); } else { $types[$class]= null; - $this->scope->import($class); + $parse->scope->import($class); } - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if ('}' === $this->token->value) { + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('}' === $parse->token->value) { break; } else { $this->expect(', or }'); } } - $this->token= $this->advance(); - } else if ('as' === $this->token->value) { - $this->token= $this->advance(); - $types= [$import => $this->token->value]; - $this->scope->import($import, $this->token->value); - $this->token= $this->advance(); + $parse->forward(); + } else if ('as' === $parse->token->value) { + $parse->forward(); + $types= [$import => $parse->token->value]; + $parse->scope->import($import, $parse->token->value); + $parse->forward(); } else { $types= [$import => null]; - $this->scope->import($import); + $parse->scope->import($import); } - $this->token= $this->next(';'); + $parse->expecting(';', 'use'); $node->value= $types; return $node; }); - $this->stmt('if', function($node) { - $this->token= $this->expect('('); - $condition= $this->expression(0); - $this->token= $this->expect(')'); + self::stmt('if', function($parse, $node) { + $parse->expecting('(', 'if'); + $condition= $parse->expression(0); + $parse->expecting(')', 'if'); - $when= $this->block(); + $when= $parse->block(); - if ('else' === $this->token->value) { - $this->token= $this->advance(); - $otherwise= $this->block(); + if ('else' === $parse->token->value) { + $parse->forward(); + $otherwise= $parse->block(); } else { $otherwise= null; } @@ -593,164 +580,164 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->stmt('switch', function($node) { - $this->token= $this->expect('('); - $condition= $this->expression(0); - $this->token= $this->expect(')'); + self::stmt('switch', function($parse, $node) { + $parse->expecting('(', 'switch'); + $condition= $parse->expression(0); + $parse->expecting(')', 'switch'); $cases= []; - $this->token= $this->expect('{'); - while ('}' !== $this->token->value) { - if ('default' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect(':'); + $parse->expecting('{', 'switch'); + while ('}' !== $parse->token->value) { + if ('default' === $parse->token->value) { + $parse->forward(); + $parse->expecting(':', 'switch'); $cases[]= new CaseLabel(null, []); - } else if ('case' === $this->token->value) { - $this->token= $this->advance(); - $expr= $this->expression(0); - $this->token= $this->expect(':'); + } else if ('case' === $parse->token->value) { + $parse->forward(); + $expr= $parse->expression(0); + $parse->expecting(':', 'switch'); $cases[]= new CaseLabel($expr, []); } else { - $cases[sizeof($cases) - 1]->body[]= $this->statement(); + $cases[sizeof($cases) - 1]->body[]= $parse->statement(); } } - $this->token= $this->advance(); + $parse->forward(); $node->value= new SwitchStatement($condition, $cases); $node->kind= 'switch'; return $node; }); - $this->stmt('break', function($node) { - if (';' === $this->token->value) { + self::stmt('break', function($parse, $node) { + if (';' === $parse->token->value) { $node->value= null; - $this->token= $this->advance(); + $parse->forward(); } else { - $node->value= $this->expression(0); - $this->token= $this->next(';'); + $node->value= $parse->expression(0); + $parse->expecting(';', 'break'); } $node->kind= 'break'; return $node; }); - $this->stmt('continue', function($node) { - if (';' === $this->token->value) { + self::stmt('continue', function($parse, $node) { + if (';' === $parse->token->value) { $node->value= null; - $this->token= $this->advance(); + $parse->forward(); } else { - $node->value= $this->expression(0); - $this->token= $this->next(';'); + $node->value= $parse->expression(0); + $parse->expecting(';', 'continue'); } $node->kind= 'continue'; return $node; }); - $this->stmt('do', function($node) { - $block= $this->block(); + self::stmt('do', function($parse, $node) { + $block= $parse->block(); - $this->token= $this->next('while'); - $this->token= $this->next('('); - $expression= $this->expression(0); - $this->token= $this->next(')'); - $this->token= $this->next(';'); + $parse->expecting('while', 'while'); + $parse->expecting('(', 'while'); + $expression= $parse->expression(0); + $parse->expecting(')', 'while'); + $parse->expecting(';', 'while'); $node->value= new DoLoop($expression, $block); $node->kind= 'do'; return $node; }); - $this->stmt('while', function($node) { - $this->token= $this->next('('); - $expression= $this->expression(0); - $this->token= $this->next(')'); - $block= $this->block(); + self::stmt('while', function($parse, $node) { + $parse->expecting('(', 'while'); + $expression= $parse->expression(0); + $parse->expecting(')', 'while'); + $block= $parse->block(); $node->value= new WhileLoop($expression, $block); $node->kind= 'while'; return $node; }); - $this->stmt('for', function($node) { - $this->token= $this->next('('); - $init= $this->arguments(';'); - $this->token= $this->next(';'); - $cond= $this->arguments(';'); - $this->token= $this->next(';'); - $loop= $this->arguments(')'); - $this->token= $this->next(')'); + self::stmt('for', function($parse, $node) { + $parse->expecting('(', 'for'); + $init= $parse->expressions(';'); + $parse->expecting(';', 'for'); + $cond= $parse->expressions(';'); + $parse->expecting(';', 'for'); + $loop= $parse->expressions(')'); + $parse->expecting(')', 'for'); - $block= $this->block(); + $block= $parse->block(); $node->value= new ForLoop($init, $cond, $loop, $block); $node->kind= 'for'; return $node; }); - $this->stmt('foreach', function($node) { - $this->token= $this->next('('); - $expression= $this->expression(0); + self::stmt('foreach', function($parse, $node) { + $parse->expecting('(', 'foreach'); + $expression= $parse->expression(0); - $this->token= $this->advance('as'); - $expr= $this->expression(0); + $parse->expecting('as', 'foreach'); + $expr= $parse->expression(0); - if ('=>' === $this->token->value) { - $this->token= $this->advance(); + if ('=>' === $parse->token->value) { + $parse->forward(); $key= $expr; - $value= $this->expression(0); + $value= $parse->expression(0); } else { $key= null; $value= $expr; } - $this->token= $this->next(')'); + $parse->expecting(')', 'foreach'); - $block= $this->block(); + $block= $parse->block(); $node->value= new ForeachLoop($expression, $key, $value, $block); $node->kind= 'foreach'; return $node; }); - $this->stmt('throw', function($node) { - $node->value= $this->expression(0); + self::stmt('throw', function($parse, $node) { + $node->value= $parse->expression(0); $node->kind= 'throw'; - $this->token= $this->next(';'); + $parse->expecting(';', 'throw'); return $node; }); - $this->stmt('try', function($node) { - $this->token= $this->next('{'); - $statements= $this->statements(); - $this->token= $this->next('}'); + self::stmt('try', function($parse, $node) { + $parse->expecting('{', 'try'); + $statements= $parse->statements(); + $parse->expecting('}', 'try'); $catches= []; - while ('catch' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->next('('); + while ('catch' === $parse->token->value) { + $parse->forward(); + $parse->expecting('(', 'try'); $types= []; - while ('name' === $this->token->kind) { - $types[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if ('|' !== $this->token->value) break; - $this->token= $this->advance(); + while ('name' === $parse->token->kind) { + $types[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if ('|' !== $parse->token->value) break; + $parse->forward(); } - $variable= $this->token; - $this->token= $this->advance(); - $this->token= $this->next(')'); + $variable= $parse->token; + $parse->forward(); + $parse->expecting(')', 'catch'); - $this->token= $this->next('{'); - $catches[]= new CatchStatement($types, $variable->value, $this->statements()); - $this->token= $this->next('}'); + $parse->expecting('{', 'catch'); + $catches[]= new CatchStatement($types, $variable->value, $parse->statements()); + $parse->expecting('}', 'catch'); } - if ('finally' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->next('{'); - $finally= $this->statements(); - $this->token= $this->next('}'); + if ('finally' === $parse->token->value) { + $parse->forward(); + $parse->expecting('{', 'finally'); + $finally= $parse->statements(); + $parse->expecting('}', 'finally'); } else { $finally= null; } @@ -760,13 +747,13 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->stmt('return', function($node) { - if (';' === $this->token->value) { + self::stmt('return', function($parse, $node) { + if (';' === $parse->token->value) { $expr= null; - $this->token= $this->advance(); + $parse->forward(); } else { - $expr= $this->expression(0); - $this->token= $this->next(';'); + $expr= $parse->expression(0); + $parse->expecting(';', 'return'); } $node->value= $expr; @@ -774,234 +761,234 @@ public function __construct($tokens, $file= null, $scope= null) { return $node; }); - $this->stmt('abstract', function($node) { - $this->token= $this->advance(); - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); + self::stmt('abstract', function($parse, $node) { + $parse->forward(); + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); - $node->value= $this->clazz($type, ['abstract']); + $node->value= $parse->clazz($type, ['abstract']); $node->kind= 'class'; return $node; }); - $this->stmt('final', function($node) { - $this->token= $this->advance(); - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); + self::stmt('final', function($parse, $node) { + $parse->forward(); + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); - $node->value= $this->clazz($type, ['final']); + $node->value= $parse->clazz($type, ['final']); $node->kind= 'class'; return $node; }); - $this->stmt('<<', function($node) { + self::stmt('<<', function($parse, $node) { do { - $name= $this->token->value; - $this->token= $this->advance(); + $name= $parse->token->value; + $parse->forward(); - if ('(' === $this->token->value) { - $this->token= $this->advance(); - $this->scope->annotations[$name]= $this->expression(0); - $this->token= $this->next(')'); + if ('(' === $parse->token->value) { + $parse->forward(); + $parse->scope->annotations[$name]= $parse->expression(0); + $parse->expecting(')', 'annotations'); } else { - $this->scope->annotations[$name]= null; + $parse->scope->annotations[$name]= null; } - if (',' === $this->token->value) { + if (',' === $parse->token->value) { continue; - } else if ('>>' === $this->token->value) { + } else if ('>>' === $parse->token->value) { break; } else { - $this->token= $this->next(', or >>', 'annotation'); + $parse->expecting(', or >>', 'annotation'); } - } while (null !== $this->token->value); + } while (null !== $parse->token->value); - $this->token= $this->advance(); + $parse->forward(); $node->kind= 'annotation'; return $node; }); - $this->stmt('class', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); + self::stmt('class', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); - $node->value= $this->clazz($type); + $node->value= $parse->clazz($type); $node->kind= 'class'; return $node; }); - $this->stmt('interface', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - $comment= $this->comment; - $this->comment= null; + self::stmt('interface', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + $comment= $parse->comment; + $parse->comment= null; $parents= []; - if ('extends' === $this->token->value) { - $this->token= $this->advance(); + if ('extends' === $parse->token->value) { + $parse->forward(); do { - $parents[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if ('{' === $this->token->value) { + $parents[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('{' === $parse->token->value) { break; } else { - $this->token= $this->next(', or {', 'interface parents'); + $parse->expecting(', or {', 'interface parents'); } - } while (null !== $this->token->value); + } while (null !== $parse->token->value); } - $this->token= $this->expect('{'); - $body= $this->typeBody(); - $this->token= $this->expect('}'); + $parse->expecting('{', 'interface'); + $body= $parse->typeBody(); + $parse->expecting('}', 'interface'); - $node->value= new InterfaceDeclaration([], $type, $parents, $body, $this->scope->annotations, $comment); + $node->value= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment); $node->kind= 'interface'; - $this->scope->annotations= []; + $parse->scope->annotations= []; return $node; }); - $this->stmt('trait', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - $comment= $this->comment; - $this->comment= null; + self::stmt('trait', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + $comment= $parse->comment; + $parse->comment= null; - $this->token= $this->expect('{'); - $body= $this->typeBody(); - $this->token= $this->expect('}'); + $parse->expecting('{', 'trait'); + $body= $parse->typeBody(); + $parse->expecting('}', 'trait'); - $node->value= new TraitDeclaration([], $type, $body, $this->scope->annotations, $comment); + $node->value= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment); $node->kind= 'trait'; - $this->scope->annotations= []; + $parse->scope->annotations= []; return $node; }); - $this->body('use', function(&$body, $annotations, $modifiers) { - $member= new Node($this->token->symbol); + self::body('use', function($parse, &$body, $annotations, $modifiers) { + $member= new Node($parse->token->symbol); $member->kind= 'use'; - $member->line= $this->token->line; + $member->line= $parse->token->line; - $this->token= $this->advance(); + $parse->forward(); $types= []; do { - $types[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->advance(); + $types[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); continue; } else { break; } - } while ($this->token->value); + } while ($parse->token->value); $aliases= []; - if ('{' === $this->token->value) { - $this->token= $this->advance(); - while ('}' !== $this->token->value) { - $method= $this->token->value; - $this->token= $this->advance(); - if ('::' === $this->token->value) { - $this->token= $this->advance(); - $method= $this->scope->resolve($method).'::'.$this->token->value; - $this->token= $this->advance(); + if ('{' === $parse->token->value) { + $parse->forward(); + while ('}' !== $parse->token->value) { + $method= $parse->token->value; + $parse->forward(); + if ('::' === $parse->token->value) { + $parse->forward(); + $method= $parse->scope->resolve($method).'::'.$parse->token->value; + $parse->forward(); } - $this->token= $this->expect('as'); - $alias= $this->token->value; - $this->token= $this->advance(); - $this->token= $this->expect(';'); + $parse->expecting('as', 'use'); + $alias= $parse->token->value; + $parse->forward(); + $parse->expecting(';', 'use'); $aliases[$method]= $alias; } - $this->token= $this->expect('}'); + $parse->expecting('}', 'use'); } else { - $this->token= $this->expect(';'); + $parse->expecting(';', 'use'); } $member->value= new UseExpression($types, $aliases); $body[]= $member; }); - $this->body('const', function(&$body, $annotations, $modifiers) { - $n= new Node($this->token->symbol); + self::body('const', function($parse, &$body, $annotations, $modifiers) { + $n= new Node($parse->token->symbol); $n->kind= 'const'; - $this->token= $this->advance(); + $parse->forward(); $type= null; - while (';' !== $this->token->value) { + while (';' !== $parse->token->value) { $member= clone $n; - $member->line= $this->token->line; - $first= $this->token; - $this->token= $this->advance(); + $member->line= $parse->token->line; + $first= $parse->token; + $parse->forward(); // Untyped `const T = 5` vs. typed `const int T = 5` - if ('=' === $this->token->value) { + if ('=' === $parse->token->value) { $name= $first->value; } else { - $this->queue[]= $first; - $this->queue[]= $this->token; - $this->token= $first; - - $type= $this->type(false); - $this->token= $this->advance(); - $name= $this->token->value; - $this->token= $this->advance(); + $parse->queue[]= $first; + $parse->queue[]= $parse->token; + $parse->token= $first; + + $type= $parse->type($parse, false); + $parse->forward(); + $name= $parse->token->value; + $parse->forward(); } if (isset($body[$name])) { - $this->raise('Cannot redeclare constant '.$name); + $parse->raise('Cannot redeclare constant '.$name); } - $this->token= $this->expect('='); - $member->value= new Constant($modifiers, $name, $type, $this->expression(0)); + $parse->token= $parse->expect('='); + $member->value= new Constant($modifiers, $name, $type, $parse->expression(0)); $body[$name]= $member; - if (',' === $this->token->value) { - $this->token= $this->expect(','); + if (',' === $parse->token->value) { + $parse->forward(); } } - $this->token= $this->expect(';', 'constant declaration'); + $parse->expecting(';', 'constant declaration'); }); - $this->body('@variable', function(&$body, $annotations, $modifiers) { - $this->properties($body, $annotations, $modifiers, null); + self::body('@variable', function($parse, &$body, $annotations, $modifiers) { + $parse->properties($parse, $body, $annotations, $modifiers, null); }); - $this->body('function', function(&$body, $annotations, $modifiers) { - $member= new Node($this->token->symbol); + self::body('function', function($parse, &$body, $annotations, $modifiers) { + $member= new Node($parse->token->symbol); $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; + $member->line= $parse->token->line; + $comment= $parse->comment; + $parse->comment= null; - $this->token= $this->advance(); - $name= $this->token->value; + $parse->forward(); + $name= $parse->token->value; $lookup= $name.'()'; if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); + $parse->raise('Cannot redeclare method '.$lookup); } - $this->token= $this->advance(); - $signature= $this->signature(); + $parse->forward(); + $signature= $parse->signature(); - if ('{' === $this->token->value) { // Regular body - $this->token= $this->advance(); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else if (';' === $this->token->value) { // Abstract or interface method + if ('{' === $parse->token->value) { // Regular body + $parse->forward(); + $statements= $parse->statements(); + $parse->expecting('}', 'method declaration'); + } else if (';' === $parse->token->value) { // Abstract or interface method $statements= null; - $this->token= $this->expect(';'); - } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - $this->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); - - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $this->token= $this->advance(); - $n->value= $this->expressionWithThrows(0); + $parse->expecting(';', 'method declaration'); + } else if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' + $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); + + $n= new Node($parse->token->symbol); + $n->line= $parse->token->line; + $parse->forward(); + $n->value= $parse->expressionWithThrows(0); $n->kind= 'return'; $statements= [$n]; - $this->token= $this->expect(';'); + $parse->expecting(';', 'method declaration'); } else { - $this->token= $this->expect('{, ; or ==>', 'method declaration'); + $parse->expecting('{, ; or ==>', 'method declaration'); } $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); @@ -1009,26 +996,39 @@ public function __construct($tokens, $file= null, $scope= null) { }); } - private function type($optional= true) { + /** + * Creates a new parse instance + * + * @param lang.ast.Tokens $tokens + * @param string $file + * @param lang.ast.Scope $scope + */ + public function __construct($tokens, $file= null, $scope= null) { + $this->tokens= $tokens->getIterator(); + $this->scope= $scope ?: new Scope(null); + $this->file= $file; + } + + private function type($parse, $optional= true) { $t= []; do { - $t[]= $this->type0($optional); + $t[]= $this->type0($parse, $optional); if ('|' === $this->token->value) { - $this->token= $this->advance(); + $parse->forward(); continue; } return 1 === sizeof($t) ? $t[0] : new UnionType($t); } while (true); } - private function type0($optional) { + private function type0($parse, $optional) { if ('?' === $this->token->value) { $this->token= $this->advance(); - $type= '?'.$this->scope->resolve($this->token->value); + $type= '?'.$parse->scope->resolve($this->token->value); $this->token= $this->advance(); } else if ('(' === $this->token->value) { $this->token= $this->advance(); - $type= $this->type(false); + $type= $this->type($parse, false); $this->token= $this->advance(); return $type; } else if ('name' === $this->token->kind && 'function' === $this->token->value) { @@ -1036,7 +1036,7 @@ private function type0($optional) { $this->token= $this->expect('('); $signature= []; if (')' !== $this->token->value) do { - $signature[]= $this->type(false); + $signature[]= $this->type($parse, false); if (',' === $this->token->value) { $this->token= $this->advance(); } else if (')' === $this->token->value) { @@ -1047,9 +1047,9 @@ private function type0($optional) { } while (null !== $this->token->value); $this->token= $this->expect(')'); $this->token= $this->expect(':'); - return new FunctionType($signature, $this->type(false)); + return new FunctionType($signature, $this->type($parse, false)); } else if ('name' === $this->token->kind) { - $type= $this->scope->resolve($this->token->value); + $type= $parse->scope->resolve($this->token->value); $this->token= $this->advance(); } else if ($optional) { return null; @@ -1061,7 +1061,7 @@ private function type0($optional) { $this->token= $this->advance(); $components= []; do { - $components[]= $this->type(false); + $components[]= $this->type($parse, false); if (',' === $this->token->value) { $this->token= $this->advance(); } else if ('>' === $this->token->symbol->id) { @@ -1083,7 +1083,7 @@ private function type0($optional) { } } - private function properties(&$body, $annotations, $modifiers, $type) { + private function properties($parse, &$body, $annotations, $modifiers, $type) { $n= new Node($this->token->symbol); $n->kind= 'property'; $comment= $this->comment; @@ -1097,7 +1097,7 @@ private function properties(&$body, $annotations, $modifiers, $type) { if ('variable' === $this->token->kind) { $name= $this->token->value; } else { - $type= $this->type(false); + $type= $this->type($parse, false); $name= $this->token->value; } @@ -1122,7 +1122,7 @@ private function properties(&$body, $annotations, $modifiers, $type) { $this->token= $this->next(';', 'field declaration'); } - private function parameters() { + private function parameters($parse) { static $promotion= ['private' => true, 'protected' => true, 'public' => true]; $parameters= []; @@ -1161,7 +1161,7 @@ private function parameters() { $promote= null; } - $type= $this->type(); + $type= $this->type($parse, ); if ('...' === $this->token->value) { $variadic= true; @@ -1194,93 +1194,216 @@ private function parameters() { return $parameters; } - private function signature() { - $this->token= $this->expect('('); - $parameters= $this->parameters(); - $this->token= $this->expect(')'); - - if (':' === $this->token->value) { - $this->token= $this->advance(); - $return= $this->type(); + // {{ setup + public static function symbol($id, $lbp= 0) { + if (isset(self::$symbols[$id])) { + $symbol= self::$symbols[$id]; + if ($lbp > $symbol->lbp) { + $symbol->lbp= $lbp; + } } else { - $return= null; + $symbol= new Symbol(); + $symbol->id= $id; + $symbol->lbp= $lbp; + self::$symbols[$id]= $symbol; } + return $symbol; + } - return new Signature($parameters, $return); + public static function constant($id, $value) { + $const= self::symbol($id); + $const->nud= function($parse, $node) use($value) { + $node->kind= 'literal'; + $node->value= $value; + return $node; + }; + return $const; } - private function block() { - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $block= $this->statements(); - $this->token= $this->next('}'); - return $block; - } else { - return [$this->statement()]; - } + public static function assignment($id) { + $infix= self::symbol($id, 10); + $infix->led= function($parse, $node, $left) use($id) { + $node->kind= 'assignment'; + $node->value= new Assignment($left, $id, $parse->expression(9)); + return $node; + }; + return $infix; } - private function expressionWithThrows($bp) { - if ('throw' === $this->token->value) { - $expr= new Node($this->token->symbol); - $expr->kind= 'throwexpression'; - $this->token= $this->advance(); - $expr->value= $this->expression($bp); - return $expr; - } else { - return $this->expression($bp); - } + public static function infix($id, $bp, $led= null) { + $infix= self::symbol($id, $bp); + $infix->led= $led ?: function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $parse->expression($bp)); + $node->kind= 'binary'; + return $node; + }; + return $infix; } - private function clazz($name, $modifiers= []) { - $comment= $this->comment; - $this->comment= null; + public static function infixr($id, $bp, $led= null) { + $infix= self::symbol($id, $bp); + $infix->led= $led ?: function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $parse->expression($bp - 1)); + $node->kind= 'binary'; + return $node; + }; + return $infix; + } - $parent= null; - if ('extends' === $this->token->value) { - $this->token= $this->advance(); - $parent= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - } + public static function infixt($id, $bp) { + $infix= self::symbol($id, $bp); + $infix->led= function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $parse->expressionWithThrows($bp - 1)); + $node->kind= 'binary'; + return $node; + }; + return $infix; + } - $implements= []; - if ('implements' === $this->token->value) { - $this->token= $this->advance(); - do { - $implements[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } else if ('{' === $this->token->value) { - break; - } else { - $this->token= $this->next(', or {', 'interfaces list'); - } - } while (null !== $this->token->value); - } + public static function prefix($id, $nud= null) { + $prefix= self::symbol($id); + $prefix->nud= $nud ?: function($parse, $node) use($id) { + $node->value= new UnaryExpression($parse->expression(70), $id); + $node->kind= 'unary'; + return $node; + }; + return $prefix; + } - $this->token= $this->expect('{'); - $body= $this->typeBody(); - $this->token= $this->expect('}'); + public static function suffix($id, $bp, $led= null) { + $suffix= self::symbol($id, $bp); + $suffix->led= $led ?: function($parse, $node, $left) use($id) { + $node->value= new UnaryExpression($left, $id); + $node->kind= 'unary'; + return $node; + }; + return $suffix; + } - $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); - $this->scope->annotations= []; - return $return; + /** + * Statement + * + * @param string $id + * @param function(lang.ast.Node): lang.ast.Node + */ + public static function stmt($id, $func) { + $stmt= self::symbol($id); + $stmt->std= $func; } - private function arguments($end= ')') { - $arguments= []; - while ($end !== $this->token->value) { - $arguments[]= $this->expression(0, false); // Undefined arguments are OK - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if ($end === $this->token->value) { - break; + /** + * Type body parsing + * + * @param string $id + * @param function([:string], [:string], string[]): void + */ + public static function body($id, $func) { + self::$body[$id]= $func; + } + // }}} + + /** + * Raise an error + * + * @param string $error + * @param string $context + * @param int $line + * @return void + */ + private function raise($message, $context= null, $line= null) { + $context && $message.= ' in '.$context; + $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); + } + + /** + * Expect a given token, raise an error if another is encountered + * + * @param string $id + * @param string $context + * @return var + */ + private function next($id, $context= null) { + if ($id === $this->token->symbol->id) return $this->advance(); + + $message= sprintf( + 'Expected "%s", have "%s"%s', + $id, + $this->token->value ?: $this->token->symbol->id, + $context ? ' in '.$context : '' + ); + $this->errors[]= new Error($message, $this->file, $this->token->line); + return $this->token; + } + + /** + * Emit a warning + * + * @param string $error + * @param string $context + * @return void + */ + private function warn($message, $context= null) { + $context && $message.= ' ('.$context.')'; + trigger_error($message.' in '.$this->file.' on line '.$this->token->line); + } + + /** + * Expect a given token, raise an error if another is encountered + * + * @param string $id + * @param string $context + * @return var + */ + private function expect($id, $context= null) { + if ($id !== $this->token->symbol->id) { + $message= sprintf( + 'Expected "%s", have "%s"%s', + $id, + $this->token->value ?: $this->token->symbol->id, + $context ? ' in '.$context : '' + ); + throw new Error($message, $this->file, $this->token->line); + } + + return $this->advance(); + } + + private function advance() { + static $line= 1; + + if ($this->queue) return array_shift($this->queue); + + while ($this->tokens->valid()) { + $type= $this->tokens->key(); + list($value, $line)= $this->tokens->current(); + $this->tokens->next(); + if ('name' === $type) { + $node= new Node(isset(self::$symbols[$value]) ? self::$symbols[$value] : self::symbol('(name)')); + $node->kind= $type; + } else if ('operator' === $type) { + $node= new Node(self::symbol($value)); + $node->kind= $type; + } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { + $node= new Node(self::symbol('(literal)')); + $node->kind= 'literal'; + } else if ('variable' === $type) { + $node= new Node(self::symbol('(variable)')); + $node->kind= 'variable'; + } else if ('comment' === $type) { + $this->comment= $value; + continue; } else { - $this->expect($end.' or ,', 'argument list'); + throw new Error('Unexpected token '.$value, $this->file, $line); } + + $node->value= $value; + $node->line= $line; + return $node; } - return $arguments; + + $node= new Node(self::symbol('(end)')); + $node->line= $line; + return $node; } /** @@ -1291,7 +1414,7 @@ private function arguments($end= ')') { * - `[modifiers] const int T = 5` * - `[modifiers] function t(): int { }` */ - private function typeBody() { + public function typeBody() { static $modifier= [ 'private' => true, 'protected' => true, @@ -1308,11 +1431,11 @@ private function typeBody() { if (isset($modifier[$this->token->value])) { $modifiers[]= $this->token->value; $this->token= $this->advance(); - } else if (isset($this->body[$k= $this->token->value]) - ? ($f= $this->body[$k]) - : (isset($this->body[$k= '@'.$this->token->kind]) ? ($f= $this->body[$k]) : null) + } else if (isset(self::$body[$k= $this->token->value]) + ? ($f= self::$body[$k]) + : (isset(self::$body[$k= '@'.$this->token->kind]) ? ($f= self::$body[$k]) : null) ) { - $f($body, $annotations, $modifiers); + $f($this, $body, $annotations, $modifiers); $modifiers= []; $annotations= []; } else if ('<<' === $this->token->symbol->id) { @@ -1339,8 +1462,8 @@ private function typeBody() { } } while (null !== $this->token->value); $this->token= $this->advance(); - } else if ($type= $this->type()) { - $this->properties($body, $annotations, $modifiers, $type); + } else if ($type= $this->type($this)) { + $this->properties($this, $body, $annotations, $modifiers, $type); $modifiers= []; } else { $this->raise(sprintf( @@ -1354,32 +1477,110 @@ private function typeBody() { return $body; } - private function expression($rbp, $nud= true) { - $t= $this->token; - $this->token= $this->advance(); - if ($nud || $t->symbol->nud) { - $left= $t->nud(); + public function signature() { + $this->token= $this->expect('('); + $parameters= $this->parameters($this); + $this->token= $this->expect(')'); + + if (':' === $this->token->value) { + $this->token= $this->advance(); + $return= $this->type($this); + } else { + $return= null; + } + + return new Signature($parameters, $return); + } + + public function block() { + if ('{' === $this->token->value) { + $this->token= $this->advance(); + $block= $this->statements(); + $this->token= $this->next('}'); + return $block; + } else { + return [$this->statement()]; + } + } + + public function clazz($name, $modifiers= []) { + $comment= $this->comment; + $this->comment= null; + + $parent= null; + if ('extends' === $this->token->value) { + $this->token= $this->advance(); + $parent= $this->scope->resolve($this->token->value); + $this->token= $this->advance(); + } + + $implements= []; + if ('implements' === $this->token->value) { + $this->token= $this->advance(); + do { + $implements[]= $this->scope->resolve($this->token->value); + $this->token= $this->advance(); + if (',' === $this->token->value) { + $this->token= $this->expect(','); + } else if ('{' === $this->token->value) { + break; + } else { + $this->token= $this->next(', or {', 'interfaces list'); + } + } while (null !== $this->token->value); + } + + $this->token= $this->expect('{'); + $body= $this->typeBody(); + $this->token= $this->expect('}'); + + $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); + $this->scope->annotations= []; + return $return; + } + + public function expressionWithThrows($bp) { + if ('throw' === $this->token->value) { + $expr= new Node($this->token->symbol); + $expr->kind= 'throwexpression'; + $this->token= $this->advance(); + $expr->value= $this->expression($bp); + return $expr; } else { - $left= $t; + return $this->expression($bp); } + } + + public function expression($rbp) { + $t= $this->token; + $this->token= $this->advance(); + $left= $t->symbol->nud ? $t->symbol->nud->__invoke($this, $t) : $t; while ($rbp < $this->token->symbol->lbp) { $t= $this->token; $this->token= $this->advance(); - $left= $t->led($left); + $left= $t->symbol->led ? $t->symbol->led->__invoke($this, $t, $left) : $t; } return $left; } - private function top() { - while (null !== $this->token->value) { - if (null === ($statement= $this->statement())) break; - yield $statement; + public function expressions($end= ')') { + $arguments= []; + while ($end !== $this->token->value) { + $arguments[]= $this->expression(0, false); // Undefined arguments are OK + if (',' === $this->token->value) { + $this->token= $this->advance(); + } else if ($end === $this->token->value) { + break; + } else { + $this->expect($end.' or ,', 'argument list'); + } } + return $arguments; } - private function statements() { + public function statements() { $statements= []; while ('}' !== $this->token->value) { if (null === ($statement= $this->statement())) break; @@ -1388,11 +1589,11 @@ private function statements() { return $statements; } - private function statement() { + public function statement() { if ($this->token->symbol->std) { $t= $this->token; - $this->token= $this->advance(); - return $t->std(); + $this->forward(); + return $t->symbol->std->__invoke($this, $t); } $expr= $this->expression(0); @@ -1400,190 +1601,24 @@ private function statement() { if (';' !== $this->token->symbol->id) { $this->raise('Missing semicolon after '.$expr->kind.' statement', null, $expr->line); } else { - $this->token= $this->advance(); + $this->forward(); } return $expr; } - // {{ setup - private static function symbol($id, $lbp= 0) { - if (isset(self::$symbols[$id])) { - $symbol= self::$symbols[$id]; - if ($lbp > $symbol->lbp) { - $symbol->lbp= $lbp; - } - } else { - $symbol= new Symbol(); - $symbol->id= $id; - $symbol->lbp= $lbp; - self::$symbols[$id]= $symbol; - } - return $symbol; - } - - public static function constant($id, $value) { - $const= self::symbol($id); - $const->nud= function($node) use($value) { - $node->kind= 'literal'; - $node->value= $value; - return $node; - }; - return $const; - } - - public function assignment($id) { - $infix= self::symbol($id, 10); - $infix->led= function($node, $left) use($id) { - $node->kind= 'assignment'; - $node->value= new Assignment($left, $id, $this->expression(9)); - return $node; - }; - return $infix; - } - - public function infix($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($bp)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public function infixr($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public function infixt($id, $bp) { - $infix= self::symbol($id, $bp); - $infix->led= function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expressionWithThrows($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public function prefix($id, $nud= null) { - $prefix= self::symbol($id); - $prefix->nud= $nud ?: function($node) use($id) { - $node->value= new UnaryExpression($this->expression(70), $id); - $node->kind= 'unary'; - return $node; - }; - return $prefix; - } - - public function suffix($id, $bp, $led= null) { - $suffix= self::symbol($id, $bp); - $suffix->led= $led ? $led->bindTo($this, self::class) : function($node, $left) use($id) { - $node->value= new UnaryExpression($left, $id); - $node->kind= 'unary'; - return $node; - }; - return $suffix; - } - - /** - * Statement - * - * @param string $id - * @param function(lang.ast.Node): lang.ast.Node - */ - public function stmt($id, $func) { - $stmt= self::symbol($id); - $stmt->std= $func->bindTo($this, self::class); - } - /** - * Type body parsing - * - * @param string $id - * @param function([:string], [:string], string[]): void - */ - public function body($id, $func) { - $this->body[$id]= $func->bindTo($this, self::class); - } - // }}} - - /** - * Raise an error - * - * @param string $error - * @param string $context - * @param int $line - * @return void - */ - private function raise($message, $context= null, $line= null) { - $context && $message.= ' in '.$context; - $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); - } - - /** - * Expect a given token, raise an error if another is encountered + * Forward this parser to the next token * - * @param string $id - * @param string $context - * @return var - */ - private function next($id, $context= null) { - if ($id === $this->token->symbol->id) return $this->advance(); - - $message= sprintf( - 'Expected "%s", have "%s"%s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context ? ' in '.$context : '' - ); - $this->errors[]= new Error($message, $this->file, $this->token->line); - return $this->token; - } - - /** - * Emit a warning - * - * @param string $error - * @param string $context * @return void */ - private function warn($message, $context= null) { - $context && $message.= ' ('.$context.')'; - trigger_error($message.' in '.$this->file.' on line '.$this->token->line); - } - - /** - * Expect a given token, raise an error if another is encountered - * - * @param string $id - * @param string $context - * @return var - */ - private function expect($id, $context= null) { - if ($id !== $this->token->symbol->id) { - $message= sprintf( - 'Expected "%s", have "%s"%s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context ? ' in '.$context : '' - ); - throw new Error($message, $this->file, $this->token->line); - } - - return $this->advance(); - } - - private function advance() { + public function forward() { static $line= 1; - if ($this->queue) return array_shift($this->queue); + if ($this->queue) { + $this->token= array_shift($this->queue); + return; + } while ($this->tokens->valid()) { $type= $this->tokens->key(); @@ -1610,12 +1645,35 @@ private function advance() { $node->value= $value; $node->line= $line; - return $node; + $this->token= $node; + return; } $node= new Node(self::symbol('(end)')); $node->line= $line; - return $node; + $this->token= $node; + } + + /** + * Forward expecting a given token, raise an error if another is encountered + * + * @param string $id + * @param string $context + * @return void + */ + public function expecting($id, $context) { + if ($id === $this->token->symbol->id) { + $this->forward(); + return; + } + + $message= sprintf( + 'Expected "%s", have "%s" in %s', + $id, + $this->token->value ?: $this->token->symbol->id, + $context + ); + $this->errors[]= new Error($message, $this->file, $this->token->line); } /** @@ -1625,10 +1683,11 @@ private function advance() { * @throws lang.ast.Errors */ public function execute() { - $this->token= $this->advance(); + $this->forward(); try { - foreach ($this->top() as $node) { - yield $node; + while (null !== $this->token->value) { + if (null === ($statement= $this->statement())) break; + yield $statement; } } catch (Error $e) { $this->errors[]= $e; diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php index 428e8508..e2868a4f 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -6,29 +6,29 @@ class CompactMethods { public function setup($parser, $emitter) { - $parser->body('fn', function(&$body, $annotations, $modifiers) { - $member= new Node($this->token->symbol); + $parser->body('fn', function($parse, &$body, $annotations, $modifiers) { + $member= new Node($parse->token->symbol); $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; + $member->line= $parse->token->line; + $comment= $parse->comment; + $parse->comment= null; - $this->token= $this->advance(); - $name= $this->token->value; + $parse->forward(); + $name= $parse->token->value; $lookup= $name.'()'; if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); + $parse->raise('Cannot redeclare method '.$lookup); } - $this->token= $this->advance(); - $signature= $this->signature(); + $parse->forward(); + $signature= $parse->signature(); - $this->token= $this->expect('=>'); - $return= new Node($this->token->symbol); - $return->line= $this->token->line; - $return->value= $this->expressionWithThrows(0); + $parse->expecting('=>', 'compact function'); + $return= new Node($parse->token->symbol); + $return->line= $parse->token->line; + $return->value= $parse->expressionWithThrows(0); $return->kind= 'return'; - $this->token= $this->expect(';'); + $parse->expecting(';', 'compact function'); $member->value= new Method($modifiers, $name, $signature, [$return], $annotations, $comment); $body[$lookup]= $member; diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php index f527186c..c31e85fa 100755 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -5,14 +5,14 @@ class NullSafe { public function setup($parser, $emitter) { - $parser->infix('?->', 80, function($node, $left) { - if ('{' === $this->token->value) { - $this->token= $this->advance(); - $expr= $this->expression(0); - $this->token= $this->next('}'); + $parser->infix('?->', 80, function($parse, $node, $left) { + if ('{' === $parse->token->value) { + $parse->forward(); + $expr= $parse->expression(0); + $parse->expecting('}', 'dynamic member'); } else { - $expr= $this->token; - $this->token= $this->advance(); + $expr= $parse->token; + $parse->forward(); } $node->value= new InstanceExpression($left, $expr); diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index 0ac89593..bfcc6bd9 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -5,14 +5,14 @@ class Using { public function setup($parser, $emitter) { - $parser->stmt('using', function($node) { - $this->token= $this->expect('('); - $arguments= $this->arguments(); - $this->token= $this->expect(')'); - - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); + $parser->stmt('using', function($parse, $node) { + $parse->expecting('(', 'using arguments'); + $arguments= $parse->expressions(')'); + $parse->expecting(')', 'using arguments'); + + $parse->expecting('{', 'using block'); + $statements= $parse->statements(); + $parse->expecting('}', 'using block'); $node->value= new UsingStatement($arguments, $statements); $node->kind= 'using'; diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 7fd84fa7..0dfd2790 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -79,7 +79,7 @@ public function unclosed_annotation() { #[@test] public function unclosed_offset() { $this->assertError( - 'Expected "]", have ";"', + 'Expected "]", have ";" in offset access', $this->parse('$a[$s[0]= 5;') ); } From 00841d570dc892508dca8e3f5c61f797ddf6d960 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 18:05:16 +0200 Subject: [PATCH 054/926] Remove deprecated advance() and next() --- src/main/php/lang/ast/Parse.class.php | 203 ++++++++++---------------- 1 file changed, 76 insertions(+), 127 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 8524b63d..041f6e28 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -174,7 +174,7 @@ static function __static() { self::infix('{', 80, function($parse, $node, $left) { $expr= $parse->expression(0); - $parse->token= $parse->next('}'); + $parse->expecting('}', 'dynamic member'); $node->value= new OffsetExpression($left, $expr); $node->kind= 'offset'; @@ -183,7 +183,7 @@ static function __static() { self::infix('?', 80, function($parse, $node, $left) { $when= $parse->expressionWithThrows(0); - $parse->token= $parse->next(':'); + $parse->expecting(':', 'ternary'); $else= $parse->expressionWithThrows(0); $node->value= new TernaryExpression($left, $when, $else); $node->kind= 'ternary'; @@ -358,12 +358,12 @@ static function __static() { self::prefix('fn', function($parse, $node) { $signature= $parse->signature(); - $parse->token= $parse->expect('=>'); + $parse->expecting('=>', 'fn'); if ('{' === $parse->token->value) { - $parse->token= $parse->expect('{'); + $parse->expecting('{', 'fn'); $statements= $parse->statements(); - $parse->token= $parse->expect('}'); + $parse->expecting('}', 'fn'); } else { $statements= $parse->expressionWithThrows(0); } @@ -395,16 +395,16 @@ static function __static() { } $parse->forward(); if (')' === $parse->token->value) break; - $parse->token= $parse->expect(',', 'use list'); + $parse->expecting(',', 'use list'); } - $parse->token= $parse->expect(')'); + $parse->expecting(')', 'closure'); } else { $use= null; } - $parse->token= $parse->expect('{'); + $parse->expecting('{', 'function'); $statements= $parse->statements(); - $parse->token= $parse->expect('}'); + $parse->expecting('}', 'function'); $node->value= new ClosureExpression($signature, $use, $statements); } else { @@ -420,11 +420,11 @@ static function __static() { $n->line= $parse->token->line; $n->kind= 'return'; $statements= [$n]; - $parse->token= $parse->next(';'); + $parse->expecting(';', 'function'); } else { // Regular function - $parse->token= $parse->expect('{'); + $parse->expecting('{', 'function'); $statements= $parse->statements(); - $parse->token= $parse->expect('}'); + $parse->expecting('}', 'function'); } $parse->queue= [$parse->token]; @@ -939,7 +939,7 @@ static function __static() { $parse->raise('Cannot redeclare constant '.$name); } - $parse->token= $parse->expect('='); + $parse->expecting('=', 'const'); $member->value= new Constant($modifiers, $name, $type, $parse->expression(0)); $body[$name]= $member; if (',' === $parse->token->value) { @@ -1023,47 +1023,47 @@ private function type($parse, $optional= true) { private function type0($parse, $optional) { if ('?' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $type= '?'.$parse->scope->resolve($this->token->value); - $this->token= $this->advance(); + $this->forward(); } else if ('(' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $type= $this->type($parse, false); - $this->token= $this->advance(); + $this->forward(); return $type; } else if ('name' === $this->token->kind && 'function' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect('('); + $this->forward(); + $this->expecting('(', 'type'); $signature= []; if (')' !== $this->token->value) do { $signature[]= $this->type($parse, false); if (',' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); } else if (')' === $this->token->value) { break; } else { - $this->token= $this->next(', or )', 'function type'); + $this->expecting(', or )', 'function type'); } } while (null !== $this->token->value); - $this->token= $this->expect(')'); - $this->token= $this->expect(':'); + $this->expecting(')', 'type'); + $this->expecting(':', 'type'); return new FunctionType($signature, $this->type($parse, false)); } else if ('name' === $this->token->kind) { $type= $parse->scope->resolve($this->token->value); - $this->token= $this->advance(); + $this->forward(); } else if ($optional) { return null; } else { - $this->expect('type name'); + $this->expecting('type name', 'type'); } if ('<' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $components= []; do { $components[]= $this->type($parse, false); if (',' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); } else if ('>' === $this->token->symbol->id) { break; } else if ('>>' === $this->token->value) { @@ -1071,7 +1071,7 @@ private function type0($parse, $optional) { break; } } while (true); - $this->token= $this->expect('>'); + $this->expecting('>', 'type'); if ('array' === $type) { return 1 === sizeof($components) ? new ArrayType($components[0]) : new MapType($components[0], $components[1]); @@ -1106,9 +1106,9 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { $this->raise('Cannot redeclare property '.$lookup); } - $this->token= $this->advance(); + $this->forward(); if ('=' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); } else { $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); @@ -1116,10 +1116,10 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { $body[$lookup]= $member; if (',' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); } } - $this->token= $this->next(';', 'field declaration'); + $this->expecting(';', 'field declaration'); } private function parameters($parse) { @@ -1130,15 +1130,15 @@ private function parameters($parse) { while (')' !== $this->token->value) { if ('<<' === $this->token->value) { do { - $this->token= $this->advance(); + $this->forward(); $name= $this->token->value; - $this->token= $this->advance(); + $this->forward(); if ('(' === $this->token->value) { - $this->token= $this->expect('('); + $this->expecting('(', 'parameters'); $annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); + $this->expecting(')', 'parameters'); } else { $annotations[$name]= null; } @@ -1148,15 +1148,15 @@ private function parameters($parse) { } else if ('>>' === $this->token->value) { break; } else { - $this->token= $this->next(', or >>', 'parameter annotation'); + $this->expecting(', or >>', 'parameter annotation'); } } while (null !== $this->token->value); - $this->token= $this->expect('>>', 'parameter annotation'); + $this->expecting('>>', 'parameter annotation'); } if ('name' === $this->token->kind && isset($promotion[$this->token->value])) { $promote= $this->token->value; - $this->token= $this->advance(); + $this->forward(); } else { $promote= null; } @@ -1165,31 +1165,37 @@ private function parameters($parse) { if ('...' === $this->token->value) { $variadic= true; - $this->token= $this->advance(); + $this->forward(); } else { $variadic= false; } if ('&' === $this->token->value) { $byref= true; - $this->token= $this->advance(); + $this->forward(); } else { $byref= false; } $name= $this->token->value; - $this->token= $this->advance(); + $this->forward(); $default= null; if ('=' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $default= $this->expression(0); } $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); - - if (')' === $this->token->value) break; - $this->token= $this->expect(',', 'parameter list'); $annotations= []; + + if (')' === $this->token->value) { + break; + } else if (',' === $this->token->value) { + $this->forward(); + continue; + } else { + $this->token= $this->expect(',', 'parameter list'); + } } return $parameters; } @@ -1310,31 +1316,11 @@ public static function body($id, $func) { * @param int $line * @return void */ - private function raise($message, $context= null, $line= null) { + public function raise($message, $context= null, $line= null) { $context && $message.= ' in '.$context; $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); } - /** - * Expect a given token, raise an error if another is encountered - * - * @param string $id - * @param string $context - * @return var - */ - private function next($id, $context= null) { - if ($id === $this->token->symbol->id) return $this->advance(); - - $message= sprintf( - 'Expected "%s", have "%s"%s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context ? ' in '.$context : '' - ); - $this->errors[]= new Error($message, $this->file, $this->token->line); - return $this->token; - } - /** * Emit a warning * @@ -1365,45 +1351,8 @@ private function expect($id, $context= null) { throw new Error($message, $this->file, $this->token->line); } - return $this->advance(); - } - - private function advance() { - static $line= 1; - - if ($this->queue) return array_shift($this->queue); - - while ($this->tokens->valid()) { - $type= $this->tokens->key(); - list($value, $line)= $this->tokens->current(); - $this->tokens->next(); - if ('name' === $type) { - $node= new Node(isset(self::$symbols[$value]) ? self::$symbols[$value] : self::symbol('(name)')); - $node->kind= $type; - } else if ('operator' === $type) { - $node= new Node(self::symbol($value)); - $node->kind= $type; - } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $node= new Node(self::symbol('(literal)')); - $node->kind= 'literal'; - } else if ('variable' === $type) { - $node= new Node(self::symbol('(variable)')); - $node->kind= 'variable'; - } else if ('comment' === $type) { - $this->comment= $value; - continue; - } else { - throw new Error('Unexpected token '.$value, $this->file, $line); - } - - $node->value= $value; - $node->line= $line; - return $node; - } - - $node= new Node(self::symbol('(end)')); - $node->line= $line; - return $node; + $this->forward(); + return $this->token; } /** @@ -1430,7 +1379,7 @@ public function typeBody() { while ('}' !== $this->token->value) { if (isset($modifier[$this->token->value])) { $modifiers[]= $this->token->value; - $this->token= $this->advance(); + $this->forward(); } else if (isset(self::$body[$k= $this->token->value]) ? ($f= self::$body[$k]) : (isset(self::$body[$k= '@'.$this->token->kind]) ? ($f= self::$body[$k]) : null) @@ -1440,15 +1389,15 @@ public function typeBody() { $annotations= []; } else if ('<<' === $this->token->symbol->id) { do { - $this->token= $this->advance(); + $this->forward(); $name= $this->token->value; - $this->token= $this->advance(); + $this->forward(); if ('(' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $annotations[$name]= $this->expression(0); - $this->token= $this->next(')'); + $this->expecting(')', 'annotations'); } else { $annotations[$name]= null; } @@ -1458,10 +1407,10 @@ public function typeBody() { } else if ('>>' === $this->token->value) { break; } else { - $this->token= $this->next(', or >>', 'annotations'); + $this->expecting(', or >>', 'annotations'); } } while (null !== $this->token->value); - $this->token= $this->advance(); + $this->forward(); } else if ($type= $this->type($this)) { $this->properties($this, $body, $annotations, $modifiers, $type); $modifiers= []; @@ -1470,7 +1419,7 @@ public function typeBody() { 'Expected a type, modifier, property, annotation, method or "}", have "%s"', $this->token->symbol->id )); - $this->token= $this->advance(); + $this->forward(); if (null === $this->token->value) break; } } @@ -1483,7 +1432,7 @@ public function signature() { $this->token= $this->expect(')'); if (':' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $return= $this->type($this); } else { $return= null; @@ -1494,9 +1443,9 @@ public function signature() { public function block() { if ('{' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $block= $this->statements(); - $this->token= $this->next('}'); + $this->expecting('}', 'block'); return $block; } else { return [$this->statement()]; @@ -1509,23 +1458,23 @@ public function clazz($name, $modifiers= []) { $parent= null; if ('extends' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); $parent= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); + $this->forward(); } $implements= []; if ('implements' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); do { $implements[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); + $this->forward(); if (',' === $this->token->value) { - $this->token= $this->expect(','); + $this->forward(); } else if ('{' === $this->token->value) { break; } else { - $this->token= $this->next(', or {', 'interfaces list'); + $this->expecting(', or {', 'interfaces list'); } } while (null !== $this->token->value); } @@ -1543,7 +1492,7 @@ public function expressionWithThrows($bp) { if ('throw' === $this->token->value) { $expr= new Node($this->token->symbol); $expr->kind= 'throwexpression'; - $this->token= $this->advance(); + $this->forward(); $expr->value= $this->expression($bp); return $expr; } else { @@ -1553,12 +1502,12 @@ public function expressionWithThrows($bp) { public function expression($rbp) { $t= $this->token; - $this->token= $this->advance(); + $this->forward(); $left= $t->symbol->nud ? $t->symbol->nud->__invoke($this, $t) : $t; while ($rbp < $this->token->symbol->lbp) { $t= $this->token; - $this->token= $this->advance(); + $this->forward(); $left= $t->symbol->led ? $t->symbol->led->__invoke($this, $t, $left) : $t; } @@ -1570,7 +1519,7 @@ public function expressions($end= ')') { while ($end !== $this->token->value) { $arguments[]= $this->expression(0, false); // Undefined arguments are OK if (',' === $this->token->value) { - $this->token= $this->advance(); + $this->forward(); } else if ($end === $this->token->value) { break; } else { From c1379cf1117c9fc060300b35cf88e7fa849567a8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 18:14:09 +0200 Subject: [PATCH 055/926] Remove deprecated expect() --- src/main/php/lang/ast/Parse.class.php | 48 ++++++++++----------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 041f6e28..33c37da3 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -542,7 +542,8 @@ static function __static() { } else if ('}' === $parse->token->value) { break; } else { - $this->expect(', or }'); + $this->expecting(', or }', 'use'); + break; } } $parse->forward(); @@ -1194,7 +1195,8 @@ private function parameters($parse) { $this->forward(); continue; } else { - $this->token= $this->expect(',', 'parameter list'); + $this->expecting(',', 'parameter list'); + break; } } return $parameters; @@ -1333,28 +1335,6 @@ private function warn($message, $context= null) { trigger_error($message.' in '.$this->file.' on line '.$this->token->line); } - /** - * Expect a given token, raise an error if another is encountered - * - * @param string $id - * @param string $context - * @return var - */ - private function expect($id, $context= null) { - if ($id !== $this->token->symbol->id) { - $message= sprintf( - 'Expected "%s", have "%s"%s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context ? ' in '.$context : '' - ); - throw new Error($message, $this->file, $this->token->line); - } - - $this->forward(); - return $this->token; - } - /** * Type body * @@ -1427,9 +1407,9 @@ public function typeBody() { } public function signature() { - $this->token= $this->expect('('); + $this->expecting('(', 'signature'); $parameters= $this->parameters($this); - $this->token= $this->expect(')'); + $this->expecting(')', 'signature'); if (':' === $this->token->value) { $this->forward(); @@ -1479,9 +1459,9 @@ public function clazz($name, $modifiers= []) { } while (null !== $this->token->value); } - $this->token= $this->expect('{'); + $this->expecting('{', 'class'); $body= $this->typeBody(); - $this->token= $this->expect('}'); + $this->expecting('}', 'class'); $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); $this->scope->annotations= []; @@ -1523,7 +1503,8 @@ public function expressions($end= ')') { } else if ($end === $this->token->value) { break; } else { - $this->expect($end.' or ,', 'argument list'); + $this->expecting($end.' or ,', 'argument list'); + break; } } return $arguments; @@ -1622,7 +1603,14 @@ public function expecting($id, $context) { $this->token->value ?: $this->token->symbol->id, $context ); - $this->errors[]= new Error($message, $this->file, $this->token->line); + $e= new Error($message, $this->file, $this->token->line); + + // Ensure we stop if we encounter the end + if (null === $this->token->value) { + throw $e; + } else { + $this->errors[]= $e; + } } /** From 2e15847fd16bca0a94b4ffd7931ac407b0e4731f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 18:15:24 +0200 Subject: [PATCH 056/926] Fix "PHP Parse error: syntax error, unexpected `)`" --- src/main/php/lang/ast/Parse.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 33c37da3..34e303e1 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1162,7 +1162,7 @@ private function parameters($parse) { $promote= null; } - $type= $this->type($parse, ); + $type= $this->type($parse); if ('...' === $this->token->value) { $variadic= true; From 3371be9c798566cffac29439de6eb1deecd3bc3c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 18:36:07 +0200 Subject: [PATCH 057/926] Set up rules explicitely --- src/main/php/lang/ast/Parse.class.php | 2 +- src/main/php/module.xp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 34e303e1..b321acbb 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -48,7 +48,7 @@ class Parse { public $comment= null; public $queue= []; - static function __static() { + public static function rules() { self::symbol(':'); self::symbol(';'); self::symbol(','); diff --git a/src/main/php/module.xp b/src/main/php/module.xp index da9384b2..4e09218d 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -17,6 +17,8 @@ module xp-framework/compiler { CompilingClassloader::$syntax[]= $class->newInstance(); } + Parse::rules(); + if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); } From e91e875163573ea53e0910bc577673298b35ab39 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:39:06 +0200 Subject: [PATCH 058/926] Extract language into its dedicated class --- src/main/php/lang/ast/Compiled.class.php | 4 +- src/main/php/lang/ast/Language.class.php | 179 ++ src/main/php/lang/ast/Parse.class.php | 1505 +---------------- src/main/php/lang/ast/language/PHP.class.php | 1331 +++++++++++++++ .../lang/ast/syntax/CompactMethods.class.php | 4 +- .../php/lang/ast/syntax/NullSafe.class.php | 2 +- src/main/php/lang/ast/syntax/Using.class.php | 4 +- .../php/xp/compiler/CompileRunner.class.php | 5 +- .../ast/unittest/emit/EmittingTest.class.php | 5 +- .../ast/unittest/parse/ParseTest.class.php | 7 +- 10 files changed, 1537 insertions(+), 1509 deletions(-) create mode 100755 src/main/php/lang/ast/Language.class.php create mode 100755 src/main/php/lang/ast/language/PHP.class.php diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index affa1c5c..317cac50 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -15,10 +15,10 @@ public static function bytes($version, $source, $file) { private static function parse($version, $in, $out, $file) { try { - $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); + $parse= new Parse(Language::named('php'), new Tokens(new StreamTokenizer($in)), $file); $emitter= self::$emit[$version]->newInstance($out); foreach (CompilingClassloader::$syntax as $syntax) { - $syntax->setup($parse, $emitter); + $syntax->setup($parse->language, $emitter); } $emitter->emit($parse->execute()); return $out; diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php new file mode 100755 index 00000000..7f051130 --- /dev/null +++ b/src/main/php/lang/ast/Language.class.php @@ -0,0 +1,179 @@ +symbols[$id])) { + $symbol= $this->symbols[$id]; + if ($lbp > $symbol->lbp) { + $symbol->lbp= $lbp; + } + } else { + $symbol= new Symbol(); + $symbol->id= $id; + $symbol->lbp= $lbp; + $this->symbols[$id]= $symbol; + } + return $symbol; + } + + public function constant($id, $value) { + $const= $this->symbol($id); + $const->nud= function($parse, $node) use($value) { + $node->kind= 'literal'; + $node->value= $value; + return $node; + }; + return $const; + } + + public function assignment($id) { + $infix= $this->symbol($id, 10); + $infix->led= function($parse, $node, $left) use($id) { + $node->kind= 'assignment'; + $node->value= new Assignment($left, $id, $this->expression($parse, 9)); + return $node; + }; + return $infix; + } + + public function infix($id, $bp, $led= null) { + $infix= $this->symbol($id, $bp); + $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $this->expression($parse, $bp)); + $node->kind= 'binary'; + return $node; + }; + return $infix; + } + + public function infixr($id, $bp, $led= null) { + $infix= $this->symbol($id, $bp); + $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $this->expression($parse, $bp - 1)); + $node->kind= 'binary'; + return $node; + }; + return $infix; + } + + public function infixt($id, $bp) { + $infix= $this->symbol($id, $bp); + $infix->led= function($parse, $node, $left) use($id, $bp) { + $node->value= new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1)); + $node->kind= 'binary'; + return $node; + }; + return $infix; + } + + public function prefix($id, $nud= null) { + $prefix= $this->symbol($id); + $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $node) use($id) { + $node->value= new UnaryExpression($this->expression($parse, 0), $id); + $node->kind= 'unary'; + return $node; + }; + return $prefix; + } + + public function suffix($id, $bp, $led= null) { + $suffix= $this->symbol($id, $bp); + $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id) { + $node->value= new UnaryExpression($left, $id); + $node->kind= 'unary'; + return $node; + }; + return $suffix; + } + + public function stmt($id, $func) { + $stmt= $this->symbol($id); + $stmt->std= $func->bindTo($this, static::class); + } + + public function expression($parse, $rbp) { + $t= $parse->token; + $parse->forward(); + $left= $t->symbol->nud ? $t->symbol->nud->__invoke($parse, $t) : $t; + + while ($rbp < $parse->token->symbol->lbp) { + $t= $parse->token; + $parse->forward(); + $left= $t->symbol->led ? $t->symbol->led->__invoke($parse, $t, $left) : $t; + } + + return $left; + } + + public function expressions($parse, $end= ')') { + $arguments= []; + while ($end !== $parse->token->value) { + $arguments[]= $this->expression($parse, 0); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ($end === $parse->token->value) { + break; + } else { + $parse->expecting($end.' or ,', 'argument list'); + break; + } + } + return $arguments; + } + + /** + * Returns a single statement + * + * @param lang.ast.Parse $parse + * @return lang.ast.Node + */ + public function statement($parse) { + if ($parse->token->symbol->std) { + $t= $parse->token; + $parse->forward(); + return $t->symbol->std->__invoke($parse, $t); + } + + $expr= $this->expression($parse, 0); + + // Check for semicolon + if (';' !== $parse->token->symbol->id) { + $parse->raise('Missing semicolon after '.$expr->kind.' statement', null, $expr->line); + } else { + $parse->forward(); + } + + return $expr; + } + + /** + * Returns a list of statements + * + * @param lang.ast.Parse $parse + * @return lang.ast.Node[] + */ + public function statements($parse) { + $statements= []; + while ('}' !== $parse->token->value) { + if (null === ($statement= $this->statement($parse))) break; + $statements[]= $statement; + } + return $statements; + } + + public static function named($name) { + if (!isset(self::$instance[$name])) { + self::$instance[$name]= Package::forName('lang.ast.language')->loadClass($name)->newinstance(); + } + return self::$instance[$name]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index b321acbb..53faa5de 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1,42 +1,5 @@ ', 40); - self::infixr('>=', 40); - self::infixr('<=>', 40); - - self::infix('+', 50); - self::infix('-', 50); - self::infix('&', 50); - self::infix('|', 50); - self::infix('^', 50); - self::suffix('++', 50); - self::suffix('--', 50); - - self::infix('*', 60); - self::infix('/', 60); - self::infix('%', 60); - self::infix('.', 60); - self::infix('**', 60); - - self::infixr('<<', 70); - self::infixr('>>', 70); - - self::infix('instanceof', 60, function($parse, $node, $left) { - if ('name' === $parse->token->kind) { - $node->value= new InstanceOfExpression($left, $parse->scope->resolve($parse->token->value)); - $parse->forward(); - } else { - $node->value= new InstanceOfExpression($left, $parse->expression(0)); - } - - $node->kind= 'instanceof'; - return $node; - }); - - self::infix('->', 80, function($parse, $node, $left) { - if ('{' === $parse->token->value) { - $parse->forward(); - $expr= $parse->expression(0); - $parse->expecting('}', 'dynamic member'); - } else { - $expr= $parse->token; - $parse->forward(); - } - - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'instance'; - return $node; - }); - - self::infix('::', 80, function($parse, $node, $left) { - $node->value= new ScopeExpression($parse->scope->resolve($left->value), $parse->token); - $node->kind= 'scope'; - $parse->forward(); - return $node; - }); - - self::infix('==>', 80, function($parse, $node, $left) { - $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - - $signature= new Signature([new Parameter($left->value, null)], null); - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $parse->statements(); - $parse->expecting('}', 'arrow function'); - } else { - $statements= $parse->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; - }); - - self::infix('(', 80, function($parse, $node, $left) { - $arguments= $parse->expressions(); - $parse->expecting(')', 'invoke expression'); - $node->value= new InvokeExpression($left, $arguments); - $node->kind= 'invoke'; - return $node; - }); - - self::infix('[', 80, function($parse, $node, $left) { - if (']' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $parse->expression(0); - $parse->expecting(']', 'offset access'); - } - - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; - }); - - self::infix('{', 80, function($parse, $node, $left) { - $expr= $parse->expression(0); - $parse->expecting('}', 'dynamic member'); - - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; - }); - - self::infix('?', 80, function($parse, $node, $left) { - $when= $parse->expressionWithThrows(0); - $parse->expecting(':', 'ternary'); - $else= $parse->expressionWithThrows(0); - $node->value= new TernaryExpression($left, $when, $else); - $node->kind= 'ternary'; - return $node; - }); - - self::prefix('@'); - self::prefix('&'); - self::prefix('~'); - self::prefix('!'); - self::prefix('+'); - self::prefix('-'); - self::prefix('++'); - self::prefix('--'); - self::prefix('clone'); - - self::assignment('='); - self::assignment('&='); - self::assignment('|='); - self::assignment('^='); - self::assignment('+='); - self::assignment('-='); - self::assignment('*='); - self::assignment('/='); - self::assignment('.='); - self::assignment('**='); - self::assignment('>>='); - self::assignment('<<='); - self::assignment('??='); - - // This is ambiguous: - // - // - An arrow function `($a) ==> $a + 1` - // - An expression surrounded by parentheses `($a ?? $b)->invoke()`; - // - A cast `(int)$a` or `(int)($a / 2)`. - // - // Resolve by looking ahead after the closing ")" - self::prefix('(', function($parse, $node) { - static $types= [ - '<' => true, - '>' => true, - ',' => true, - '?' => true, - ':' => true - ]; - - $skipped= [$node, $parse->token]; - $cast= true; - $level= 1; - while ($level > 0 && null !== $parse->token->value) { - if ('(' === $parse->token->value) { - $level++; - } else if (')' === $parse->token->value) { - $level--; - } else if ('name' !== $parse->token->kind && !isset($types[$parse->token->value])) { - $cast= false; - } - $parse->forward(); - $skipped[]= $parse->token; - } - $parse->queue= array_merge($skipped, $parse->queue); - - if (':' === $parse->token->value || '==>' === $parse->token->value) { - $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - - $node->kind= 'lambda'; - $parse->forward(); - $signature= $parse->signature(); - $parse->forward(); - - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $parse->statements(); - $parse->expecting('}', 'arrow function'); - } else { - $statements= $parse->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { - $node->kind= 'cast'; - - $parse->forward(); - $parse->expecting('(', 'cast'); - $type= $parse->type0($parse, false); - $parse->expecting(')', 'cast'); - $node->value= new CastExpression($type, $parse->expression(0)); - } else { - $node->kind= 'braced'; - - $parse->forward(); - $parse->expecting('(', 'braced'); - $node->value= $parse->expression(0); - $parse->expecting(')', 'braced'); - } - return $node; - }); - - self::prefix('[', function($parse, $node) { - $values= []; - while (']' !== $parse->token->value) { - $expr= $parse->expression(0); - - if ('=>' === $parse->token->value) { - $parse->forward(); - $values[]= [$expr, $parse->expression(0)]; - } else { - $values[]= [null, $expr]; - } - - if (']' === $parse->token->value) { - break; - } else { - $parse->expecting(',', 'array literal'); - } - } - - $parse->expecting(']', 'array literal'); - $node->kind= 'array'; - $node->value= $values; - return $node; - }); - - self::prefix('new', function($parse, $node) { - $type= $parse->token; - $parse->forward(); - - $parse->expecting('(', 'new arguments'); - $arguments= $parse->expressions(); - $parse->expecting(')', 'new arguments'); - - if ('variable' === $type->kind) { - $node->value= new NewExpression('$'.$type->value, $arguments); - $node->kind= 'new'; - } else if ('class' === $type->value) { - $node->value= new NewClassExpression($parse->clazz(null), $arguments); - $node->kind= 'newclass'; - } else { - $node->value= new NewExpression($parse->scope->resolve($type->value), $arguments); - $node->kind= 'new'; - } - return $node; - }); - - self::prefix('yield', function($parse, $node) { - if (';' === $parse->token->value) { - $node->kind= 'yield'; - $node->value= new YieldExpression(null, null); - } else if ('from' === $parse->token->value) { - $parse->forward(); - $node->kind= 'from'; - $node->value= $parse->expression(0); - } else { - $node->kind= 'yield'; - $expr= $parse->expression(0); - if ('=>' === $parse->token->value) { - $parse->forward(); - $node->value= new YieldExpression($expr, $parse->expression(0)); - } else { - $node->value= new YieldExpression(null, $expr); - } - } - return $node; - }); - - self::prefix('...', function($parse, $node) { - $node->kind= 'unpack'; - $node->value= $parse->expression(0); - return $node; - }); - - self::prefix('fn', function($parse, $node) { - $signature= $parse->signature(); - - $parse->expecting('=>', 'fn'); - - if ('{' === $parse->token->value) { - $parse->expecting('{', 'fn'); - $statements= $parse->statements(); - $parse->expecting('}', 'fn'); - } else { - $statements= $parse->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; - }); - - - self::prefix('function', function($parse, $node) { - - // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; - // the latter explicitely becomes a statement by pushing a semicolon. - if ('(' === $parse->token->value) { - $node->kind= 'closure'; - $signature= $parse->signature(); - - if ('use' === $parse->token->value) { - $parse->forward(); - $parse->forward(); - $use= []; - while (')' !== $parse->token->value) { - if ('&' === $parse->token->value) { - $parse->forward(); - $use[]= '&$'.$parse->token->value; - } else { - $use[]= '$'.$parse->token->value; - } - $parse->forward(); - if (')' === $parse->token->value) break; - $parse->expecting(',', 'use list'); - } - $parse->expecting(')', 'closure'); - } else { - $use= null; - } - - $parse->expecting('{', 'function'); - $statements= $parse->statements(); - $parse->expecting('}', 'function'); - - $node->value= new ClosureExpression($signature, $use, $statements); - } else { - $node->kind= 'function'; - $name= $parse->token->value; - $parse->forward(); - $signature= $parse->signature(); - - if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' - $n= new Node($parse->token->symbol); - $parse->forward(); - $n->value= $parse->expressionWithThrows(0); - $n->line= $parse->token->line; - $n->kind= 'return'; - $statements= [$n]; - $parse->expecting(';', 'function'); - } else { // Regular function - $parse->expecting('{', 'function'); - $statements= $parse->statements(); - $parse->expecting('}', 'function'); - } - - $parse->queue= [$parse->token]; - $parse->token= new Node(self::symbol(';')); - $node->value= new FunctionDeclaration($name, $signature, $statements); - } - - return $node; - }); - - self::prefix('static', function($parse, $node) { - if ('variable' === $parse->token->kind) { - $node->kind= 'static'; - $node->value= []; - while (';' !== $parse->token->value) { - $variable= $parse->token->value; - $parse->forward(); - - if ('=' === $parse->token->value) { - $parse->forward(); - $initial= $parse->expression(0); - } else { - $initial= null; - } - - $node->value[$variable]= $initial; - if (',' === $parse->token->value) { - $parse->forward(); - } - } - } - return $node; - }); - - self::prefix('goto', function($parse, $node) { - $node->kind= 'goto'; - $node->value= $parse->token->value; - $parse->forward(); - return $node; - }); - - self::prefix('(name)', function($parse, $node) { - if (':' === $parse->token->value) { - $node->kind= 'label'; - $parse->token= new Node(self::symbol(';')); - } - return $node; - }); - - self::stmt('kind= 'start'; - $node->value= $parse->token->value; - - $parse->forward(); - return $node; - }); - - self::stmt('{', function($parse, $node) { - $node->kind= 'block'; - $node->value= $parse->statements(); - $parse->forward(); - return $node; - }); - - self::prefix('echo', function($parse, $node) { - $node->kind= 'echo'; - $node->value= $parse->expressions(';'); - return $node; - }); - - self::stmt('namespace', function($parse, $node) { - $node->kind= 'package'; - $node->value= $parse->token->value; - - $parse->forward(); - $parse->expecting(';', 'namespace'); - - $parse->scope->package($node->value); - return $node; - }); - - self::stmt('use', function($parse, $node) { - if ('function' === $parse->token->value) { - $node->kind= 'importfunction'; - $parse->forward(); - } else if ('const' === $parse->token->value) { - $node->kind= 'importconst'; - $parse->forward(); - } else { - $node->kind= 'import'; - } - - $import= $parse->token->value; - $parse->forward(); - - if ('{' === $parse->token->value) { - $types= []; - $parse->forward(); - while ('}' !== $parse->token->value) { - $class= $import.$parse->token->value; - - $parse->forward(); - if ('as' === $parse->token->value) { - $parse->forward(); - $types[$class]= $parse->token->value; - $parse->scope->import($parse->token->value); - $parse->forward(); - } else { - $types[$class]= null; - $parse->scope->import($class); - } - - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('}' === $parse->token->value) { - break; - } else { - $this->expecting(', or }', 'use'); - break; - } - } - $parse->forward(); - } else if ('as' === $parse->token->value) { - $parse->forward(); - $types= [$import => $parse->token->value]; - $parse->scope->import($import, $parse->token->value); - $parse->forward(); - } else { - $types= [$import => null]; - $parse->scope->import($import); - } - - $parse->expecting(';', 'use'); - $node->value= $types; - return $node; - }); - - self::stmt('if', function($parse, $node) { - $parse->expecting('(', 'if'); - $condition= $parse->expression(0); - $parse->expecting(')', 'if'); - - $when= $parse->block(); - - if ('else' === $parse->token->value) { - $parse->forward(); - $otherwise= $parse->block(); - } else { - $otherwise= null; - } - - $node->value= new IfStatement($condition, $when, $otherwise); - $node->kind= 'if'; - return $node; - }); - - self::stmt('switch', function($parse, $node) { - $parse->expecting('(', 'switch'); - $condition= $parse->expression(0); - $parse->expecting(')', 'switch'); - - $cases= []; - $parse->expecting('{', 'switch'); - while ('}' !== $parse->token->value) { - if ('default' === $parse->token->value) { - $parse->forward(); - $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel(null, []); - } else if ('case' === $parse->token->value) { - $parse->forward(); - $expr= $parse->expression(0); - $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel($expr, []); - } else { - $cases[sizeof($cases) - 1]->body[]= $parse->statement(); - } - } - $parse->forward(); - - $node->value= new SwitchStatement($condition, $cases); - $node->kind= 'switch'; - return $node; - }); - - self::stmt('break', function($parse, $node) { - if (';' === $parse->token->value) { - $node->value= null; - $parse->forward(); - } else { - $node->value= $parse->expression(0); - $parse->expecting(';', 'break'); - } - - $node->kind= 'break'; - return $node; - }); - - self::stmt('continue', function($parse, $node) { - if (';' === $parse->token->value) { - $node->value= null; - $parse->forward(); - } else { - $node->value= $parse->expression(0); - $parse->expecting(';', 'continue'); - } - - $node->kind= 'continue'; - return $node; - }); - - self::stmt('do', function($parse, $node) { - $block= $parse->block(); - - $parse->expecting('while', 'while'); - $parse->expecting('(', 'while'); - $expression= $parse->expression(0); - $parse->expecting(')', 'while'); - $parse->expecting(';', 'while'); - - $node->value= new DoLoop($expression, $block); - $node->kind= 'do'; - return $node; - }); - - self::stmt('while', function($parse, $node) { - $parse->expecting('(', 'while'); - $expression= $parse->expression(0); - $parse->expecting(')', 'while'); - $block= $parse->block(); - - $node->value= new WhileLoop($expression, $block); - $node->kind= 'while'; - return $node; - }); - - self::stmt('for', function($parse, $node) { - $parse->expecting('(', 'for'); - $init= $parse->expressions(';'); - $parse->expecting(';', 'for'); - $cond= $parse->expressions(';'); - $parse->expecting(';', 'for'); - $loop= $parse->expressions(')'); - $parse->expecting(')', 'for'); - - $block= $parse->block(); - - $node->value= new ForLoop($init, $cond, $loop, $block); - $node->kind= 'for'; - return $node; - }); - - self::stmt('foreach', function($parse, $node) { - $parse->expecting('(', 'foreach'); - $expression= $parse->expression(0); - - $parse->expecting('as', 'foreach'); - $expr= $parse->expression(0); - - if ('=>' === $parse->token->value) { - $parse->forward(); - $key= $expr; - $value= $parse->expression(0); - } else { - $key= null; - $value= $expr; - } - - $parse->expecting(')', 'foreach'); - - $block= $parse->block(); - $node->value= new ForeachLoop($expression, $key, $value, $block); - $node->kind= 'foreach'; - return $node; - }); - - self::stmt('throw', function($parse, $node) { - $node->value= $parse->expression(0); - $node->kind= 'throw'; - $parse->expecting(';', 'throw'); - return $node; - }); - - self::stmt('try', function($parse, $node) { - $parse->expecting('{', 'try'); - $statements= $parse->statements(); - $parse->expecting('}', 'try'); - - $catches= []; - while ('catch' === $parse->token->value) { - $parse->forward(); - $parse->expecting('(', 'try'); - - $types= []; - while ('name' === $parse->token->kind) { - $types[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if ('|' !== $parse->token->value) break; - $parse->forward(); - } - - $variable= $parse->token; - $parse->forward(); - $parse->expecting(')', 'catch'); - - $parse->expecting('{', 'catch'); - $catches[]= new CatchStatement($types, $variable->value, $parse->statements()); - $parse->expecting('}', 'catch'); - } - - if ('finally' === $parse->token->value) { - $parse->forward(); - $parse->expecting('{', 'finally'); - $finally= $parse->statements(); - $parse->expecting('}', 'finally'); - } else { - $finally= null; - } - - $node->value= new TryStatement($statements, $catches, $finally); - $node->kind= 'try'; - return $node; - }); - - self::stmt('return', function($parse, $node) { - if (';' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $parse->expression(0); - $parse->expecting(';', 'return'); - } - - $node->value= $expr; - $node->kind= 'return'; - return $node; - }); - - self::stmt('abstract', function($parse, $node) { - $parse->forward(); - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - - $node->value= $parse->clazz($type, ['abstract']); - $node->kind= 'class'; - return $node; - }); - - self::stmt('final', function($parse, $node) { - $parse->forward(); - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - - $node->value= $parse->clazz($type, ['final']); - $node->kind= 'class'; - return $node; - }); - - self::stmt('<<', function($parse, $node) { - do { - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->forward(); - $parse->scope->annotations[$name]= $parse->expression(0); - $parse->expecting(')', 'annotations'); - } else { - $parse->scope->annotations[$name]= null; - } - - if (',' === $parse->token->value) { - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', 'annotation'); - } - } while (null !== $parse->token->value); - - $parse->forward(); - $node->kind= 'annotation'; - return $node; - }); - - self::stmt('class', function($parse, $node) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - - $node->value= $parse->clazz($type); - $node->kind= 'class'; - return $node; - }); - - self::stmt('interface', function($parse, $node) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - $comment= $parse->comment; - $parse->comment= null; - - $parents= []; - if ('extends' === $parse->token->value) { - $parse->forward(); - do { - $parents[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('{' === $parse->token->value) { - break; - } else { - $parse->expecting(', or {', 'interface parents'); - } - } while (null !== $parse->token->value); - } - - $parse->expecting('{', 'interface'); - $body= $parse->typeBody(); - $parse->expecting('}', 'interface'); - - $node->value= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment); - $node->kind= 'interface'; - $parse->scope->annotations= []; - return $node; - }); - - self::stmt('trait', function($parse, $node) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - $comment= $parse->comment; - $parse->comment= null; - - $parse->expecting('{', 'trait'); - $body= $parse->typeBody(); - $parse->expecting('}', 'trait'); - - $node->value= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment); - $node->kind= 'trait'; - $parse->scope->annotations= []; - return $node; - }); - - self::body('use', function($parse, &$body, $annotations, $modifiers) { - $member= new Node($parse->token->symbol); - $member->kind= 'use'; - $member->line= $parse->token->line; - - $parse->forward(); - $types= []; - do { - $types[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else { - break; - } - } while ($parse->token->value); - - $aliases= []; - if ('{' === $parse->token->value) { - $parse->forward(); - while ('}' !== $parse->token->value) { - $method= $parse->token->value; - $parse->forward(); - if ('::' === $parse->token->value) { - $parse->forward(); - $method= $parse->scope->resolve($method).'::'.$parse->token->value; - $parse->forward(); - } - $parse->expecting('as', 'use'); - $alias= $parse->token->value; - $parse->forward(); - $parse->expecting(';', 'use'); - $aliases[$method]= $alias; - } - $parse->expecting('}', 'use'); - } else { - $parse->expecting(';', 'use'); - } - - $member->value= new UseExpression($types, $aliases); - $body[]= $member; - }); - - self::body('const', function($parse, &$body, $annotations, $modifiers) { - $n= new Node($parse->token->symbol); - $n->kind= 'const'; - $parse->forward(); - - $type= null; - while (';' !== $parse->token->value) { - $member= clone $n; - $member->line= $parse->token->line; - $first= $parse->token; - $parse->forward(); - - // Untyped `const T = 5` vs. typed `const int T = 5` - if ('=' === $parse->token->value) { - $name= $first->value; - } else { - $parse->queue[]= $first; - $parse->queue[]= $parse->token; - $parse->token= $first; - - $type= $parse->type($parse, false); - $parse->forward(); - $name= $parse->token->value; - $parse->forward(); - } - - if (isset($body[$name])) { - $parse->raise('Cannot redeclare constant '.$name); - } - - $parse->expecting('=', 'const'); - $member->value= new Constant($modifiers, $name, $type, $parse->expression(0)); - $body[$name]= $member; - if (',' === $parse->token->value) { - $parse->forward(); - } - } - $parse->expecting(';', 'constant declaration'); - }); - - self::body('@variable', function($parse, &$body, $annotations, $modifiers) { - $parse->properties($parse, $body, $annotations, $modifiers, null); - }); - - self::body('function', function($parse, &$body, $annotations, $modifiers) { - $member= new Node($parse->token->symbol); - $member->kind= 'method'; - $member->line= $parse->token->line; - $comment= $parse->comment; - $parse->comment= null; - - $parse->forward(); - $name= $parse->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $parse->raise('Cannot redeclare method '.$lookup); - } - - $parse->forward(); - $signature= $parse->signature(); - - if ('{' === $parse->token->value) { // Regular body - $parse->forward(); - $statements= $parse->statements(); - $parse->expecting('}', 'method declaration'); - } else if (';' === $parse->token->value) { // Abstract or interface method - $statements= null; - $parse->expecting(';', 'method declaration'); - } else if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' - $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); - - $n= new Node($parse->token->symbol); - $n->line= $parse->token->line; - $parse->forward(); - $n->value= $parse->expressionWithThrows(0); - $n->kind= 'return'; - $statements= [$n]; - $parse->expecting(';', 'method declaration'); - } else { - $parse->expecting('{, ; or ==>', 'method declaration'); - } - - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; - }); - } + public static function rules() { /* NOOP */ } /** * Creates a new parse instance @@ -1004,312 +20,13 @@ public static function rules() { * @param string $file * @param lang.ast.Scope $scope */ - public function __construct($tokens, $file= null, $scope= null) { + public function __construct($language, $tokens, $file= null, $scope= null) { + $this->language= $language; $this->tokens= $tokens->getIterator(); $this->scope= $scope ?: new Scope(null); $this->file= $file; } - private function type($parse, $optional= true) { - $t= []; - do { - $t[]= $this->type0($parse, $optional); - if ('|' === $this->token->value) { - $parse->forward(); - continue; - } - return 1 === sizeof($t) ? $t[0] : new UnionType($t); - } while (true); - } - - private function type0($parse, $optional) { - if ('?' === $this->token->value) { - $this->forward(); - $type= '?'.$parse->scope->resolve($this->token->value); - $this->forward(); - } else if ('(' === $this->token->value) { - $this->forward(); - $type= $this->type($parse, false); - $this->forward(); - return $type; - } else if ('name' === $this->token->kind && 'function' === $this->token->value) { - $this->forward(); - $this->expecting('(', 'type'); - $signature= []; - if (')' !== $this->token->value) do { - $signature[]= $this->type($parse, false); - if (',' === $this->token->value) { - $this->forward(); - } else if (')' === $this->token->value) { - break; - } else { - $this->expecting(', or )', 'function type'); - } - } while (null !== $this->token->value); - $this->expecting(')', 'type'); - $this->expecting(':', 'type'); - return new FunctionType($signature, $this->type($parse, false)); - } else if ('name' === $this->token->kind) { - $type= $parse->scope->resolve($this->token->value); - $this->forward(); - } else if ($optional) { - return null; - } else { - $this->expecting('type name', 'type'); - } - - if ('<' === $this->token->value) { - $this->forward(); - $components= []; - do { - $components[]= $this->type($parse, false); - if (',' === $this->token->value) { - $this->forward(); - } else if ('>' === $this->token->symbol->id) { - break; - } else if ('>>' === $this->token->value) { - $this->queue[]= $this->token= new Node(self::symbol('>')); - break; - } - } while (true); - $this->expecting('>', 'type'); - - if ('array' === $type) { - return 1 === sizeof($components) ? new ArrayType($components[0]) : new MapType($components[0], $components[1]); - } else { - return new GenericType($type, $components); - } - } else { - return new Type($type); - } - } - - private function properties($parse, &$body, $annotations, $modifiers, $type) { - $n= new Node($this->token->symbol); - $n->kind= 'property'; - $comment= $this->comment; - $this->comment= null; - - while (';' !== $this->token->value) { - $member= clone $n; - $member->line= $this->token->line; - - // Untyped `$a` vs. typed `int $a` - if ('variable' === $this->token->kind) { - $name= $this->token->value; - } else { - $type= $this->type($parse, false); - $name= $this->token->value; - } - - $lookup= '$'.$name; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare property '.$lookup); - } - - $this->forward(); - if ('=' === $this->token->value) { - $this->forward(); - $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); - } else { - $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); - } - - $body[$lookup]= $member; - if (',' === $this->token->value) { - $this->forward(); - } - } - $this->expecting(';', 'field declaration'); - } - - private function parameters($parse) { - static $promotion= ['private' => true, 'protected' => true, 'public' => true]; - - $parameters= []; - $annotations= []; - while (')' !== $this->token->value) { - if ('<<' === $this->token->value) { - do { - $this->forward(); - - $name= $this->token->value; - $this->forward(); - - if ('(' === $this->token->value) { - $this->expecting('(', 'parameters'); - $annotations[$name]= $this->expression(0); - $this->expecting(')', 'parameters'); - } else { - $annotations[$name]= null; - } - - if (',' === $this->token->value) { - continue; - } else if ('>>' === $this->token->value) { - break; - } else { - $this->expecting(', or >>', 'parameter annotation'); - } - } while (null !== $this->token->value); - $this->expecting('>>', 'parameter annotation'); - } - - if ('name' === $this->token->kind && isset($promotion[$this->token->value])) { - $promote= $this->token->value; - $this->forward(); - } else { - $promote= null; - } - - $type= $this->type($parse); - - if ('...' === $this->token->value) { - $variadic= true; - $this->forward(); - } else { - $variadic= false; - } - - if ('&' === $this->token->value) { - $byref= true; - $this->forward(); - } else { - $byref= false; - } - - $name= $this->token->value; - $this->forward(); - - $default= null; - if ('=' === $this->token->value) { - $this->forward(); - $default= $this->expression(0); - } - $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); - $annotations= []; - - if (')' === $this->token->value) { - break; - } else if (',' === $this->token->value) { - $this->forward(); - continue; - } else { - $this->expecting(',', 'parameter list'); - break; - } - } - return $parameters; - } - - // {{ setup - public static function symbol($id, $lbp= 0) { - if (isset(self::$symbols[$id])) { - $symbol= self::$symbols[$id]; - if ($lbp > $symbol->lbp) { - $symbol->lbp= $lbp; - } - } else { - $symbol= new Symbol(); - $symbol->id= $id; - $symbol->lbp= $lbp; - self::$symbols[$id]= $symbol; - } - return $symbol; - } - - public static function constant($id, $value) { - $const= self::symbol($id); - $const->nud= function($parse, $node) use($value) { - $node->kind= 'literal'; - $node->value= $value; - return $node; - }; - return $const; - } - - public static function assignment($id) { - $infix= self::symbol($id, 10); - $infix->led= function($parse, $node, $left) use($id) { - $node->kind= 'assignment'; - $node->value= new Assignment($left, $id, $parse->expression(9)); - return $node; - }; - return $infix; - } - - public static function infix($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $parse->expression($bp)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public static function infixr($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $parse->expression($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public static function infixt($id, $bp) { - $infix= self::symbol($id, $bp); - $infix->led= function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $parse->expressionWithThrows($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - public static function prefix($id, $nud= null) { - $prefix= self::symbol($id); - $prefix->nud= $nud ?: function($parse, $node) use($id) { - $node->value= new UnaryExpression($parse->expression(70), $id); - $node->kind= 'unary'; - return $node; - }; - return $prefix; - } - - public static function suffix($id, $bp, $led= null) { - $suffix= self::symbol($id, $bp); - $suffix->led= $led ?: function($parse, $node, $left) use($id) { - $node->value= new UnaryExpression($left, $id); - $node->kind= 'unary'; - return $node; - }; - return $suffix; - } - - /** - * Statement - * - * @param string $id - * @param function(lang.ast.Node): lang.ast.Node - */ - public static function stmt($id, $func) { - $stmt= self::symbol($id); - $stmt->std= $func; - } - - /** - * Type body parsing - * - * @param string $id - * @param function([:string], [:string], string[]): void - */ - public static function body($id, $func) { - self::$body[$id]= $func; - } - // }}} - /** * Raise an error * @@ -1330,213 +47,11 @@ public function raise($message, $context= null, $line= null) { * @param string $context * @return void */ - private function warn($message, $context= null) { + public function warn($message, $context= null) { $context && $message.= ' ('.$context.')'; trigger_error($message.' in '.$this->file.' on line '.$this->token->line); } - /** - * Type body - * - * - `use [traits]` - * - `[modifiers] int $t = 5` - * - `[modifiers] const int T = 5` - * - `[modifiers] function t(): int { }` - */ - public function typeBody() { - static $modifier= [ - 'private' => true, - 'protected' => true, - 'public' => true, - 'static' => true, - 'final' => true, - 'abstract' => true - ]; - - $body= []; - $modifiers= []; - $annotations= []; - while ('}' !== $this->token->value) { - if (isset($modifier[$this->token->value])) { - $modifiers[]= $this->token->value; - $this->forward(); - } else if (isset(self::$body[$k= $this->token->value]) - ? ($f= self::$body[$k]) - : (isset(self::$body[$k= '@'.$this->token->kind]) ? ($f= self::$body[$k]) : null) - ) { - $f($this, $body, $annotations, $modifiers); - $modifiers= []; - $annotations= []; - } else if ('<<' === $this->token->symbol->id) { - do { - $this->forward(); - - $name= $this->token->value; - $this->forward(); - - if ('(' === $this->token->value) { - $this->forward(); - $annotations[$name]= $this->expression(0); - $this->expecting(')', 'annotations'); - } else { - $annotations[$name]= null; - } - - if (',' === $this->token->value) { - continue; - } else if ('>>' === $this->token->value) { - break; - } else { - $this->expecting(', or >>', 'annotations'); - } - } while (null !== $this->token->value); - $this->forward(); - } else if ($type= $this->type($this)) { - $this->properties($this, $body, $annotations, $modifiers, $type); - $modifiers= []; - } else { - $this->raise(sprintf( - 'Expected a type, modifier, property, annotation, method or "}", have "%s"', - $this->token->symbol->id - )); - $this->forward(); - if (null === $this->token->value) break; - } - } - return $body; - } - - public function signature() { - $this->expecting('(', 'signature'); - $parameters= $this->parameters($this); - $this->expecting(')', 'signature'); - - if (':' === $this->token->value) { - $this->forward(); - $return= $this->type($this); - } else { - $return= null; - } - - return new Signature($parameters, $return); - } - - public function block() { - if ('{' === $this->token->value) { - $this->forward(); - $block= $this->statements(); - $this->expecting('}', 'block'); - return $block; - } else { - return [$this->statement()]; - } - } - - public function clazz($name, $modifiers= []) { - $comment= $this->comment; - $this->comment= null; - - $parent= null; - if ('extends' === $this->token->value) { - $this->forward(); - $parent= $this->scope->resolve($this->token->value); - $this->forward(); - } - - $implements= []; - if ('implements' === $this->token->value) { - $this->forward(); - do { - $implements[]= $this->scope->resolve($this->token->value); - $this->forward(); - if (',' === $this->token->value) { - $this->forward(); - } else if ('{' === $this->token->value) { - break; - } else { - $this->expecting(', or {', 'interfaces list'); - } - } while (null !== $this->token->value); - } - - $this->expecting('{', 'class'); - $body= $this->typeBody(); - $this->expecting('}', 'class'); - - $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); - $this->scope->annotations= []; - return $return; - } - - public function expressionWithThrows($bp) { - if ('throw' === $this->token->value) { - $expr= new Node($this->token->symbol); - $expr->kind= 'throwexpression'; - $this->forward(); - $expr->value= $this->expression($bp); - return $expr; - } else { - return $this->expression($bp); - } - } - - public function expression($rbp) { - $t= $this->token; - $this->forward(); - $left= $t->symbol->nud ? $t->symbol->nud->__invoke($this, $t) : $t; - - while ($rbp < $this->token->symbol->lbp) { - $t= $this->token; - $this->forward(); - $left= $t->symbol->led ? $t->symbol->led->__invoke($this, $t, $left) : $t; - } - - return $left; - } - - public function expressions($end= ')') { - $arguments= []; - while ($end !== $this->token->value) { - $arguments[]= $this->expression(0, false); // Undefined arguments are OK - if (',' === $this->token->value) { - $this->forward(); - } else if ($end === $this->token->value) { - break; - } else { - $this->expecting($end.' or ,', 'argument list'); - break; - } - } - return $arguments; - } - - public function statements() { - $statements= []; - while ('}' !== $this->token->value) { - if (null === ($statement= $this->statement())) break; - $statements[]= $statement; - } - return $statements; - } - - public function statement() { - if ($this->token->symbol->std) { - $t= $this->token; - $this->forward(); - return $t->symbol->std->__invoke($this, $t); - } - - $expr= $this->expression(0); - - if (';' !== $this->token->symbol->id) { - $this->raise('Missing semicolon after '.$expr->kind.' statement', null, $expr->line); - } else { - $this->forward(); - } - - return $expr; - } - /** * Forward this parser to the next token * @@ -1555,16 +70,16 @@ public function forward() { list($value, $line)= $this->tokens->current(); $this->tokens->next(); if ('name' === $type) { - $node= new Node(isset(self::$symbols[$value]) ? self::$symbols[$value] : self::symbol('(name)')); + $node= new Node(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); $node->kind= $type; } else if ('operator' === $type) { - $node= new Node(self::symbol($value)); + $node= new Node($this->language->symbol($value)); $node->kind= $type; } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $node= new Node(self::symbol('(literal)')); + $node= new Node($this->language->symbol('(literal)')); $node->kind= 'literal'; } else if ('variable' === $type) { - $node= new Node(self::symbol('(variable)')); + $node= new Node($this->language->symbol('(variable)')); $node->kind= 'variable'; } else if ('comment' === $type) { $this->comment= $value; @@ -1579,7 +94,7 @@ public function forward() { return; } - $node= new Node(self::symbol('(end)')); + $node= new Node($this->language->symbol('(end)')); $node->line= $line; $this->token= $node; } @@ -1623,7 +138,7 @@ public function execute() { $this->forward(); try { while (null !== $this->token->value) { - if (null === ($statement= $this->statement())) break; + if (null === ($statement= $this->language->statement($this))) break; yield $statement; } } catch (Error $e) { diff --git a/src/main/php/lang/ast/language/PHP.class.php b/src/main/php/lang/ast/language/PHP.class.php new file mode 100755 index 00000000..771196cd --- /dev/null +++ b/src/main/php/lang/ast/language/PHP.class.php @@ -0,0 +1,1331 @@ +symbol(':'); + $this->symbol(';'); + $this->symbol(','); + $this->symbol(')'); + $this->symbol(']'); + $this->symbol('}'); + $this->symbol('as'); + $this->symbol('const'); + $this->symbol('(end)'); + $this->symbol('(name)'); + $this->symbol('(literal)'); + $this->symbol('(variable)'); + + $this->constant('true', 'true'); + $this->constant('false', 'false'); + $this->constant('null', 'null'); + + $this->infixt('??', 30); + $this->infixt('?:', 30); + $this->infixr('&&', 30); + $this->infixr('||', 30); + + $this->infixr('==', 40); + $this->infixr('===', 40); + $this->infixr('!=', 40); + $this->infixr('!==', 40); + $this->infixr('<', 40); + $this->infixr('<=', 40); + $this->infixr('>', 40); + $this->infixr('>=', 40); + $this->infixr('<=>', 40); + + $this->infix('+', 50); + $this->infix('-', 50); + $this->infix('&', 50); + $this->infix('|', 50); + $this->infix('^', 50); + $this->suffix('++', 50); + $this->suffix('--', 50); + + $this->infix('*', 60); + $this->infix('/', 60); + $this->infix('%', 60); + $this->infix('.', 60); + $this->infix('**', 60); + + $this->infixr('<<', 70); + $this->infixr('>>', 70); + + $this->infix('instanceof', 60, function($parse, $node, $left) { + if ('name' === $parse->token->kind) { + $node->value= new InstanceOfExpression($left, $parse->scope->resolve($parse->token->value)); + $parse->forward(); + } else { + $node->value= new InstanceOfExpression($left, $this->expression($parse, 0)); + } + + $node->kind= 'instanceof'; + return $node; + }); + + $this->infix('->', 80, function($parse, $node, $left) { + if ('{' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + $parse->expecting('}', 'dynamic member'); + } else { + $expr= $parse->token; + $parse->forward(); + } + + $node->value= new InstanceExpression($left, $expr); + $node->kind= 'instance'; + return $node; + }); + + $this->infix('::', 80, function($parse, $node, $left) { + $node->value= new ScopeExpression($parse->scope->resolve($left->value), $parse->token); + $node->kind= 'scope'; + $parse->forward(); + return $node; + }); + + $this->infix('==>', 80, function($parse, $node, $left) { + $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + + $signature= new Signature([new Parameter($left->value, null)], null); + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'arrow function'); + } else { + $statements= $this->expressionWithThrows($parse, 0); + } + + $node->value= new LambdaExpression($signature, $statements); + $node->kind= 'lambda'; + return $node; + }); + + $this->infix('(', 80, function($parse, $node, $left) { + $arguments= $this->expressions($parse, ); + $parse->expecting(')', 'invoke expression'); + $node->value= new InvokeExpression($left, $arguments); + $node->kind= 'invoke'; + return $node; + }); + + $this->infix('[', 80, function($parse, $node, $left) { + if (']' === $parse->token->value) { + $expr= null; + $parse->forward(); + } else { + $expr= $this->expression($parse, 0); + $parse->expecting(']', 'offset access'); + } + + $node->value= new OffsetExpression($left, $expr); + $node->kind= 'offset'; + return $node; + }); + + $this->infix('{', 80, function($parse, $node, $left) { + $expr= $this->expression($parse, 0); + $parse->expecting('}', 'dynamic member'); + + $node->value= new OffsetExpression($left, $expr); + $node->kind= 'offset'; + return $node; + }); + + $this->infix('?', 80, function($parse, $node, $left) { + $when= $this->expressionWithThrows($parse, 0); + $parse->expecting(':', 'ternary'); + $else= $this->expressionWithThrows($parse, 0); + $node->value= new TernaryExpression($left, $when, $else); + $node->kind= 'ternary'; + return $node; + }); + + $this->prefix('@'); + $this->prefix('&'); + $this->prefix('~'); + $this->prefix('!'); + $this->prefix('+'); + $this->prefix('-'); + $this->prefix('++'); + $this->prefix('--'); + $this->prefix('clone'); + + $this->assignment('='); + $this->assignment('&='); + $this->assignment('|='); + $this->assignment('^='); + $this->assignment('+='); + $this->assignment('-='); + $this->assignment('*='); + $this->assignment('/='); + $this->assignment('.='); + $this->assignment('**='); + $this->assignment('>>='); + $this->assignment('<<='); + $this->assignment('??='); + + // This is ambiguous: + // + // - An arrow function `($a) ==> $a + 1` + // - An expression surrounded by parentheses `($a ?? $b)->invoke()`; + // - A cast `(int)$a` or `(int)($a / 2)`. + // + // Resolve by looking ahead after the closing ")" + $this->prefix('(', function($parse, $node) { + static $types= [ + '<' => true, + '>' => true, + ',' => true, + '?' => true, + ':' => true + ]; + + $skipped= [$node, $parse->token]; + $cast= true; + $level= 1; + while ($level > 0 && null !== $parse->token->value) { + if ('(' === $parse->token->value) { + $level++; + } else if (')' === $parse->token->value) { + $level--; + } else if ('name' !== $parse->token->kind && !isset($types[$parse->token->value])) { + $cast= false; + } + $parse->forward(); + $skipped[]= $parse->token; + } + $parse->queue= array_merge($skipped, $parse->queue); + + if (':' === $parse->token->value || '==>' === $parse->token->value) { + $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + + $node->kind= 'lambda'; + $parse->forward(); + $signature= $this->signature($parse); + $parse->forward(); + + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'arrow function'); + } else { + $statements= $this->expressionWithThrows($parse, 0); + } + + $node->value= new LambdaExpression($signature, $statements); + } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { + $node->kind= 'cast'; + + $parse->forward(); + $parse->expecting('(', 'cast'); + $type= $this->type0($parse, false); + $parse->expecting(')', 'cast'); + $node->value= new CastExpression($type, $this->expression($parse, 0)); + } else { + $node->kind= 'braced'; + + $parse->forward(); + $parse->expecting('(', 'braced'); + $node->value= $this->expression($parse, 0); + $parse->expecting(')', 'braced'); + } + return $node; + }); + + $this->prefix('[', function($parse, $node) { + $values= []; + while (']' !== $parse->token->value) { + $expr= $this->expression($parse, 0); + + if ('=>' === $parse->token->value) { + $parse->forward(); + $values[]= [$expr, $this->expression($parse, 0)]; + } else { + $values[]= [null, $expr]; + } + + if (']' === $parse->token->value) { + break; + } else { + $parse->expecting(',', 'array literal'); + } + } + + $parse->expecting(']', 'array literal'); + $node->kind= 'array'; + $node->value= $values; + return $node; + }); + + $this->prefix('new', function($parse, $node) { + $type= $parse->token; + $parse->forward(); + + $parse->expecting('(', 'new arguments'); + $arguments= $this->expressions($parse, ); + $parse->expecting(')', 'new arguments'); + + if ('variable' === $type->kind) { + $node->value= new NewExpression('$'.$type->value, $arguments); + $node->kind= 'new'; + } else if ('class' === $type->value) { + $node->value= new NewClassExpression($this->clazz($parse, null), $arguments); + $node->kind= 'newclass'; + } else { + $node->value= new NewExpression($parse->scope->resolve($type->value), $arguments); + $node->kind= 'new'; + } + return $node; + }); + + $this->prefix('yield', function($parse, $node) { + if (';' === $parse->token->value) { + $node->kind= 'yield'; + $node->value= new YieldExpression(null, null); + } else if ('from' === $parse->token->value) { + $parse->forward(); + $node->kind= 'from'; + $node->value= $this->expression($parse, 0); + } else { + $node->kind= 'yield'; + $expr= $this->expression($parse, 0); + if ('=>' === $parse->token->value) { + $parse->forward(); + $node->value= new YieldExpression($expr, $this->expression($parse, 0)); + } else { + $node->value= new YieldExpression(null, $expr); + } + } + return $node; + }); + + $this->prefix('...', function($parse, $node) { + $node->kind= 'unpack'; + $node->value= $this->expression($parse, 0); + return $node; + }); + + $this->prefix('fn', function($parse, $node) { + $signature= $this->signature($parse); + + $parse->expecting('=>', 'fn'); + + if ('{' === $parse->token->value) { + $parse->expecting('{', 'fn'); + $statements= $this->statements($parse); + $parse->expecting('}', 'fn'); + } else { + $statements= $this->expressionWithThrows($parse, 0); + } + + $node->value= new LambdaExpression($signature, $statements); + $node->kind= 'lambda'; + return $node; + }); + + + $this->prefix('function', function($parse, $node) { + + // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; + // the latter explicitely becomes a statement by pushing a semicolon. + if ('(' === $parse->token->value) { + $node->kind= 'closure'; + $signature= $this->signature($parse); + + if ('use' === $parse->token->value) { + $parse->forward(); + $parse->forward(); + $use= []; + while (')' !== $parse->token->value) { + if ('&' === $parse->token->value) { + $parse->forward(); + $use[]= '&$'.$parse->token->value; + } else { + $use[]= '$'.$parse->token->value; + } + $parse->forward(); + if (')' === $parse->token->value) break; + $parse->expecting(',', 'use list'); + } + $parse->expecting(')', 'closure'); + } else { + $use= null; + } + + $parse->expecting('{', 'function'); + $statements= $this->statements($parse); + $parse->expecting('}', 'function'); + + $node->value= new ClosureExpression($signature, $use, $statements); + } else { + $node->kind= 'function'; + $name= $parse->token->value; + $parse->forward(); + $signature= $this->signature($parse); + + if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' + $n= new Node($parse->token->symbol); + $parse->forward(); + $n->value= $this->expressionWithThrows($parse, 0); + $n->line= $parse->token->line; + $n->kind= 'return'; + $statements= [$n]; + $parse->expecting(';', 'function'); + } else { // Regular function + $parse->expecting('{', 'function'); + $statements= $this->statements($parse); + $parse->expecting('}', 'function'); + } + + $parse->queue= [$parse->token]; + $parse->token= new Node($this->symbol(';')); + $node->value= new FunctionDeclaration($name, $signature, $statements); + } + + return $node; + }); + + $this->prefix('static', function($parse, $node) { + if ('variable' === $parse->token->kind) { + $node->kind= 'static'; + $node->value= []; + while (';' !== $parse->token->value) { + $variable= $parse->token->value; + $parse->forward(); + + if ('=' === $parse->token->value) { + $parse->forward(); + $initial= $this->expression($parse, 0); + } else { + $initial= null; + } + + $node->value[$variable]= $initial; + if (',' === $parse->token->value) { + $parse->forward(); + } + } + } + return $node; + }); + + $this->prefix('goto', function($parse, $node) { + $node->kind= 'goto'; + $node->value= $parse->token->value; + $parse->forward(); + return $node; + }); + + $this->prefix('(name)', function($parse, $node) { + if (':' === $parse->token->value) { + $node->kind= 'label'; + $parse->token= new Node($this->symbol(';')); + } + return $node; + }); + + $this->stmt('kind= 'start'; + $node->value= $parse->token->value; + + $parse->forward(); + return $node; + }); + + $this->stmt('{', function($parse, $node) { + $node->kind= 'block'; + $node->value= $this->statements($parse); + $parse->forward(); + return $node; + }); + + $this->prefix('echo', function($parse, $node) { + $node->kind= 'echo'; + $node->value= $this->expressions($parse, ';'); + return $node; + }); + + $this->stmt('namespace', function($parse, $node) { + $node->kind= 'package'; + $node->value= $parse->token->value; + + $parse->forward(); + $parse->expecting(';', 'namespace'); + + $parse->scope->package($node->value); + return $node; + }); + + $this->stmt('use', function($parse, $node) { + if ('function' === $parse->token->value) { + $node->kind= 'importfunction'; + $parse->forward(); + } else if ('const' === $parse->token->value) { + $node->kind= 'importconst'; + $parse->forward(); + } else { + $node->kind= 'import'; + } + + $import= $parse->token->value; + $parse->forward(); + + if ('{' === $parse->token->value) { + $types= []; + $parse->forward(); + while ('}' !== $parse->token->value) { + $class= $import.$parse->token->value; + + $parse->forward(); + if ('as' === $parse->token->value) { + $parse->forward(); + $types[$class]= $parse->token->value; + $parse->scope->import($parse->token->value); + $parse->forward(); + } else { + $types[$class]= null; + $parse->scope->import($class); + } + + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('}' === $parse->token->value) { + break; + } else { + $this->expecting(', or }', 'use'); + break; + } + } + $parse->forward(); + } else if ('as' === $parse->token->value) { + $parse->forward(); + $types= [$import => $parse->token->value]; + $parse->scope->import($import, $parse->token->value); + $parse->forward(); + } else { + $types= [$import => null]; + $parse->scope->import($import); + } + + $parse->expecting(';', 'use'); + $node->value= $types; + return $node; + }); + + $this->stmt('if', function($parse, $node) { + $parse->expecting('(', 'if'); + $condition= $this->expression($parse, 0); + $parse->expecting(')', 'if'); + + $when= $this->block($parse); + + if ('else' === $parse->token->value) { + $parse->forward(); + $otherwise= $this->block($parse); + } else { + $otherwise= null; + } + + $node->value= new IfStatement($condition, $when, $otherwise); + $node->kind= 'if'; + return $node; + }); + + $this->stmt('switch', function($parse, $node) { + $parse->expecting('(', 'switch'); + $condition= $this->expression($parse, 0); + $parse->expecting(')', 'switch'); + + $cases= []; + $parse->expecting('{', 'switch'); + while ('}' !== $parse->token->value) { + if ('default' === $parse->token->value) { + $parse->forward(); + $parse->expecting(':', 'switch'); + $cases[]= new CaseLabel(null, []); + } else if ('case' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + $parse->expecting(':', 'switch'); + $cases[]= new CaseLabel($expr, []); + } else { + $cases[sizeof($cases) - 1]->body[]= $this->statement($parse); + } + } + $parse->forward(); + + $node->value= new SwitchStatement($condition, $cases); + $node->kind= 'switch'; + return $node; + }); + + $this->stmt('break', function($parse, $node) { + if (';' === $parse->token->value) { + $node->value= null; + $parse->forward(); + } else { + $node->value= $this->expression($parse, 0); + $parse->expecting(';', 'break'); + } + + $node->kind= 'break'; + return $node; + }); + + $this->stmt('continue', function($parse, $node) { + if (';' === $parse->token->value) { + $node->value= null; + $parse->forward(); + } else { + $node->value= $this->expression($parse, 0); + $parse->expecting(';', 'continue'); + } + + $node->kind= 'continue'; + return $node; + }); + + $this->stmt('do', function($parse, $node) { + $block= $this->block($parse); + + $parse->expecting('while', 'while'); + $parse->expecting('(', 'while'); + $expression= $this->expression($parse, 0); + $parse->expecting(')', 'while'); + $parse->expecting(';', 'while'); + + $node->value= new DoLoop($expression, $block); + $node->kind= 'do'; + return $node; + }); + + $this->stmt('while', function($parse, $node) { + $parse->expecting('(', 'while'); + $expression= $this->expression($parse, 0); + $parse->expecting(')', 'while'); + $block= $this->block($parse); + + $node->value= new WhileLoop($expression, $block); + $node->kind= 'while'; + return $node; + }); + + $this->stmt('for', function($parse, $node) { + $parse->expecting('(', 'for'); + $init= $this->expressions($parse, ';'); + $parse->expecting(';', 'for'); + $cond= $this->expressions($parse, ';'); + $parse->expecting(';', 'for'); + $loop= $this->expressions($parse, ')'); + $parse->expecting(')', 'for'); + + $block= $this->block($parse); + + $node->value= new ForLoop($init, $cond, $loop, $block); + $node->kind= 'for'; + return $node; + }); + + $this->stmt('foreach', function($parse, $node) { + $parse->expecting('(', 'foreach'); + $expression= $this->expression($parse, 0); + + $parse->expecting('as', 'foreach'); + $expr= $this->expression($parse, 0); + + if ('=>' === $parse->token->value) { + $parse->forward(); + $key= $expr; + $value= $this->expression($parse, 0); + } else { + $key= null; + $value= $expr; + } + + $parse->expecting(')', 'foreach'); + + $block= $this->block($parse); + $node->value= new ForeachLoop($expression, $key, $value, $block); + $node->kind= 'foreach'; + return $node; + }); + + $this->stmt('throw', function($parse, $node) { + $node->value= $this->expression($parse, 0); + $node->kind= 'throw'; + $parse->expecting(';', 'throw'); + return $node; + }); + + $this->stmt('try', function($parse, $node) { + $parse->expecting('{', 'try'); + $statements= $this->statements($parse); + $parse->expecting('}', 'try'); + + $catches= []; + while ('catch' === $parse->token->value) { + $parse->forward(); + $parse->expecting('(', 'try'); + + $types= []; + while ('name' === $parse->token->kind) { + $types[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if ('|' !== $parse->token->value) break; + $parse->forward(); + } + + $variable= $parse->token; + $parse->forward(); + $parse->expecting(')', 'catch'); + + $parse->expecting('{', 'catch'); + $catches[]= new CatchStatement($types, $variable->value, $this->statements($parse)); + $parse->expecting('}', 'catch'); + } + + if ('finally' === $parse->token->value) { + $parse->forward(); + $parse->expecting('{', 'finally'); + $finally= $this->statements($parse); + $parse->expecting('}', 'finally'); + } else { + $finally= null; + } + + $node->value= new TryStatement($statements, $catches, $finally); + $node->kind= 'try'; + return $node; + }); + + $this->stmt('return', function($parse, $node) { + if (';' === $parse->token->value) { + $expr= null; + $parse->forward(); + } else { + $expr= $this->expression($parse, 0); + $parse->expecting(';', 'return'); + } + + $node->value= $expr; + $node->kind= 'return'; + return $node; + }); + + $this->stmt('abstract', function($parse, $node) { + $parse->forward(); + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + + $node->value= $this->clazz($parse, $type, ['abstract']); + $node->kind= 'class'; + return $node; + }); + + $this->stmt('final', function($parse, $node) { + $parse->forward(); + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + + $node->value= $this->clazz($parse, $type, ['final']); + $node->kind= 'class'; + return $node; + }); + + $this->stmt('<<', function($parse, $node) { + do { + $name= $parse->token->value; + $parse->forward(); + + if ('(' === $parse->token->value) { + $parse->forward(); + $parse->scope->annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', 'annotations'); + } else { + $parse->scope->annotations[$name]= null; + } + + if (',' === $parse->token->value) { + continue; + } else if ('>>' === $parse->token->value) { + break; + } else { + $parse->expecting(', or >>', 'annotation'); + } + } while (null !== $parse->token->value); + + $parse->forward(); + $node->kind= 'annotation'; + return $node; + }); + + $this->stmt('class', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + + $node->value= $this->clazz($parse, $type); + $node->kind= 'class'; + return $node; + }); + + $this->stmt('interface', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + $comment= $parse->comment; + $parse->comment= null; + + $parents= []; + if ('extends' === $parse->token->value) { + $parse->forward(); + do { + $parents[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('{' === $parse->token->value) { + break; + } else { + $parse->expecting(', or {', 'interface parents'); + } + } while (null !== $parse->token->value); + } + + $parse->expecting('{', 'interface'); + $body= $this->typeBody($parse); + $parse->expecting('}', 'interface'); + + $node->value= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment); + $node->kind= 'interface'; + $parse->scope->annotations= []; + return $node; + }); + + $this->stmt('trait', function($parse, $node) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + $comment= $parse->comment; + $parse->comment= null; + + $parse->expecting('{', 'trait'); + $body= $this->typeBody($parse); + $parse->expecting('}', 'trait'); + + $node->value= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment); + $node->kind= 'trait'; + $parse->scope->annotations= []; + return $node; + }); + + $this->body('use', function($parse, &$body, $annotations, $modifiers) { + $member= new Node($parse->token->symbol); + $member->kind= 'use'; + $member->line= $parse->token->line; + + $parse->forward(); + $types= []; + do { + $types[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); + continue; + } else { + break; + } + } while ($parse->token->value); + + $aliases= []; + if ('{' === $parse->token->value) { + $parse->forward(); + while ('}' !== $parse->token->value) { + $method= $parse->token->value; + $parse->forward(); + if ('::' === $parse->token->value) { + $parse->forward(); + $method= $parse->scope->resolve($method).'::'.$parse->token->value; + $parse->forward(); + } + $parse->expecting('as', 'use'); + $alias= $parse->token->value; + $parse->forward(); + $parse->expecting(';', 'use'); + $aliases[$method]= $alias; + } + $parse->expecting('}', 'use'); + } else { + $parse->expecting(';', 'use'); + } + + $member->value= new UseExpression($types, $aliases); + $body[]= $member; + }); + + $this->body('const', function($parse, &$body, $annotations, $modifiers) { + $n= new Node($parse->token->symbol); + $n->kind= 'const'; + $parse->forward(); + + $type= null; + while (';' !== $parse->token->value) { + $member= clone $n; + $member->line= $parse->token->line; + $first= $parse->token; + $parse->forward(); + + // Untyped `const T = 5` vs. typed `const int T = 5` + if ('=' === $parse->token->value) { + $name= $first->value; + } else { + $parse->queue[]= $first; + $parse->queue[]= $parse->token; + $parse->token= $first; + + $type= $this->type($parse, false); + $parse->forward(); + $name= $parse->token->value; + $parse->forward(); + } + + if (isset($body[$name])) { + $parse->raise('Cannot redeclare constant '.$name); + } + + $parse->expecting('=', 'const'); + $member->value= new Constant($modifiers, $name, $type, $this->expression($parse, 0)); + $body[$name]= $member; + if (',' === $parse->token->value) { + $parse->forward(); + } + } + $parse->expecting(';', 'constant declaration'); + }); + + $this->body('@variable', function($parse, &$body, $annotations, $modifiers) { + $this->properties($parse, $body, $annotations, $modifiers, null); + }); + + $this->body('function', function($parse, &$body, $annotations, $modifiers) { + $member= new Node($parse->token->symbol); + $member->kind= 'method'; + $member->line= $parse->token->line; + $comment= $parse->comment; + $parse->comment= null; + + $parse->forward(); + $name= $parse->token->value; + $lookup= $name.'()'; + if (isset($body[$lookup])) { + $parse->raise('Cannot redeclare method '.$lookup); + } + + $parse->forward(); + $signature= $this->signature($parse); + + if ('{' === $parse->token->value) { // Regular body + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'method declaration'); + } else if (';' === $parse->token->value) { // Abstract or interface method + $statements= null; + $parse->expecting(';', 'method declaration'); + } else if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' + $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); + + $n= new Node($parse->token->symbol); + $n->line= $parse->token->line; + $parse->forward(); + $n->value= $this->expressionWithThrows($parse, 0); + $n->kind= 'return'; + $statements= [$n]; + $parse->expecting(';', 'method declaration'); + } else { + $parse->expecting('{, ; or ==>', 'method declaration'); + } + + $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); + $body[$lookup]= $member; + }); + } + + private function type($parse, $optional= true) { + $t= []; + do { + $t[]= $this->type0($parse, $optional); + if ('|' === $parse->token->value) { + $parse->forward(); + continue; + } + return 1 === sizeof($t) ? $t[0] : new UnionType($t); + } while (true); + } + + private function type0($parse, $optional) { + if ('?' === $parse->token->value) { + $parse->forward(); + $type= '?'.$parse->scope->resolve($parse->token->value); + $parse->forward(); + } else if ('(' === $parse->token->value) { + $parse->forward(); + $type= $this->type($parse, false); + $parse->forward(); + return $type; + } else if ('name' === $parse->token->kind && 'function' === $parse->token->value) { + $parse->forward(); + $parse->expecting('(', 'type'); + $signature= []; + if (')' !== $parse->token->value) do { + $signature[]= $this->type($parse, false); + if (',' === $parse->token->value) { + $parse->forward(); + } else if (')' === $parse->token->value) { + break; + } else { + $parse->expecting(', or )', 'function type'); + } + } while (null !== $parse->token->value); + $parse->expecting(')', 'type'); + $parse->expecting(':', 'type'); + return new FunctionType($signature, $this->type($parse, false)); + } else if ('name' === $parse->token->kind) { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + } else if ($optional) { + return null; + } else { + $parse->expecting('type name', 'type'); + return null; + } + + if ('<' === $parse->token->value) { + $parse->forward(); + $components= []; + do { + $components[]= $this->type($parse, false); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('>' === $parse->token->symbol->id) { + break; + } else if ('>>' === $parse->token->value) { + $parse->queue[]= $parse->token= new Node(self::symbol('>')); + break; + } + } while (true); + $parse->expecting('>', 'type'); + + if ('array' === $type) { + return 1 === sizeof($components) ? new ArrayType($components[0]) : new MapType($components[0], $components[1]); + } else { + return new GenericType($type, $components); + } + } else { + return new Type($type); + } + } + + private function properties($parse, &$body, $annotations, $modifiers, $type) { + $n= new Node($parse->token->symbol); + $n->kind= 'property'; + $comment= $parse->comment; + $parse->comment= null; + + while (';' !== $parse->token->value) { + $member= clone $n; + $member->line= $parse->token->line; + + // Untyped `$a` vs. typed `int $a` + if ('variable' === $parse->token->kind) { + $name= $parse->token->value; + } else { + $type= $this->type($parse, false); + $name= $parse->token->value; + } + + $lookup= '$'.$name; + if (isset($body[$lookup])) { + $parse->raise('Cannot redeclare property '.$lookup); + } + + $parse->forward(); + if ('=' === $parse->token->value) { + $parse->forward(); + $member->value= new Property($modifiers, $name, $type, $this->expression($parse, 0), $annotations, $comment); + } else { + $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); + } + + $body[$lookup]= $member; + if (',' === $parse->token->value) { + $parse->forward(); + } + } + $parse->expecting(';', 'field declaration'); + } + + private function parameters($parse) { + static $promotion= ['private' => true, 'protected' => true, 'public' => true]; + + $parameters= []; + $annotations= []; + while (')' !== $parse->token->value) { + if ('<<' === $parse->token->value) { + do { + $parse->forward(); + + $name= $parse->token->value; + $parse->forward(); + + if ('(' === $parse->token->value) { + $parse->expecting('(', 'parameters'); + $annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', 'parameters'); + } else { + $annotations[$name]= null; + } + + if (',' === $parse->token->value) { + continue; + } else if ('>>' === $parse->token->value) { + break; + } else { + $parse->expecting(', or >>', 'parameter annotation'); + } + } while (null !== $parse->token->value); + $parse->expecting('>>', 'parameter annotation'); + } + + if ('name' === $parse->token->kind && isset($promotion[$parse->token->value])) { + $promote= $parse->token->value; + $parse->forward(); + } else { + $promote= null; + } + + $type= $this->type($parse); + + if ('...' === $parse->token->value) { + $variadic= true; + $parse->forward(); + } else { + $variadic= false; + } + + if ('&' === $parse->token->value) { + $byref= true; + $parse->forward(); + } else { + $byref= false; + } + + $name= $parse->token->value; + $parse->forward(); + + $default= null; + if ('=' === $parse->token->value) { + $parse->forward(); + $default= $this->expression($parse, 0); + } + $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); + $annotations= []; + + if (')' === $parse->token->value) { + break; + } else if (',' === $parse->token->value) { + $parse->forward(); + continue; + } else { + $parse->expecting(',', 'parameter list'); + break; + } + } + return $parameters; + } + + public function body($id, $func) { + $this->body[$id]= $func->bindTo($this, static::class); + } + + public function typeBody($parse) { + static $modifier= [ + 'private' => true, + 'protected' => true, + 'public' => true, + 'static' => true, + 'final' => true, + 'abstract' => true + ]; + + $body= []; + $modifiers= []; + $annotations= []; + while ('}' !== $parse->token->value) { + if (isset($modifier[$parse->token->value])) { + $modifiers[]= $parse->token->value; + $parse->forward(); + } else if (isset($this->body[$k= $parse->token->value]) + ? ($f= $this->body[$k]) + : (isset($this->body[$k= '@'.$parse->token->kind]) ? ($f= $this->body[$k]) : null) + ) { + $f($parse, $body, $annotations, $modifiers); + $modifiers= []; + $annotations= []; + } else if ('<<' === $parse->token->symbol->id) { + do { + $parse->forward(); + + $name= $parse->token->value; + $parse->forward(); + + if ('(' === $parse->token->value) { + $parse->forward(); + $annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', 'annotations'); + } else { + $annotations[$name]= null; + } + + if (',' === $parse->token->value) { + continue; + } else if ('>>' === $parse->token->value) { + break; + } else { + $parse->expecting(', or >>', 'annotations'); + } + } while (null !== $parse->token->value); + $parse->forward(); + } else if ($type= $this->type($parse)) { + $this->properties($parse, $body, $annotations, $modifiers, $type); + $modifiers= []; + } else { + $parse->raise(sprintf( + 'Expected a type, modifier, property, annotation, method or "}", have "%s"', + $parse->token->symbol->id + )); + $parse->forward(); + if (null === $parse->token->value) break; + } + } + return $body; + } + + public function signature($parse) { + $parse->expecting('(', 'signature'); + $parameters= $this->parameters($parse); + $parse->expecting(')', 'signature'); + + if (':' === $parse->token->value) { + $parse->forward(); + $return= $this->type($parse); + } else { + $return= null; + } + + return new Signature($parameters, $return); + } + + public function block($parse) { + if ('{' === $parse->token->value) { + $parse->forward(); + $block= $this->statements($parse); + $parse->expecting('}', 'block'); + return $block; + } else { + return [$this->statement($parse)]; + } + } + + public function clazz($parse, $name, $modifiers= []) { + $comment= $parse->comment; + $parse->comment= null; + + $parent= null; + if ('extends' === $parse->token->value) { + $parse->forward(); + $parent= $parse->scope->resolve($parse->token->value); + $parse->forward(); + } + + $implements= []; + if ('implements' === $parse->token->value) { + $parse->forward(); + do { + $implements[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ('{' === $parse->token->value) { + break; + } else { + $parse->expecting(', or {', 'interfaces list'); + } + } while (null !== $parse->token->value); + } + + $parse->expecting('{', 'class'); + $body= $this->typeBody($parse); + $parse->expecting('}', 'class'); + + $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $parse->scope->annotations, $comment); + $parse->scope->annotations= []; + return $return; + } + + public function expressionWithThrows($parse, $bp) { + if ('throw' === $parse->token->value) { + $expr= new Node($parse->token->symbol); + $expr->kind= 'throwexpression'; + $parse->forward(); + $expr->value= $this->expression($parse, $bp); + return $expr; + } else { + return $this->expression($parse, $bp); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php index e2868a4f..3d6c8f5b 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -21,12 +21,12 @@ public function setup($parser, $emitter) { } $parse->forward(); - $signature= $parse->signature(); + $signature= $this->signature($parse); $parse->expecting('=>', 'compact function'); $return= new Node($parse->token->symbol); $return->line= $parse->token->line; - $return->value= $parse->expressionWithThrows(0); + $return->value= $this->expressionWithThrows($parse, 0); $return->kind= 'return'; $parse->expecting(';', 'compact function'); diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php index c31e85fa..9796b753 100755 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -8,7 +8,7 @@ public function setup($parser, $emitter) { $parser->infix('?->', 80, function($parse, $node, $left) { if ('{' === $parse->token->value) { $parse->forward(); - $expr= $parse->expression(0); + $expr= $this->expression($parse, 0); $parse->expecting('}', 'dynamic member'); } else { $expr= $parse->token; diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index bfcc6bd9..4def82a1 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -7,11 +7,11 @@ class Using { public function setup($parser, $emitter) { $parser->stmt('using', function($parse, $node) { $parse->expecting('(', 'using arguments'); - $arguments= $parse->expressions(')'); + $arguments= $this->expressions($parse, ')'); $parse->expecting(')', 'using arguments'); $parse->expecting('{', 'using block'); - $statements= $parse->statements(); + $statements= $this->statements($parse); $parse->expecting('}', 'using block'); $node->value= new UsingStatement($arguments, $statements); diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index bde069ba..502dbf18 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -5,6 +5,7 @@ use lang\ast\CompilingClassloader; use lang\ast\Emitter; use lang\ast\Errors; +use lang\ast\Language; use lang\ast\Parse; use lang\ast\Tokens; use text\StreamTokenizer; @@ -76,10 +77,10 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); + $parse= new Parse(Language::named('PHP'), new Tokens(new StreamTokenizer($in)), $file); $emitter= $emit->newInstance($output->target((string)$path)); foreach (CompilingClassloader::$syntax as $syntax) { - $syntax->setup($parse, $emitter); + $syntax->setup($parse->language, $emitter); } $emitter->emit($parse->execute()); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 3ab5b8e4..dd10ab96 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -5,6 +5,7 @@ use lang\DynamicClassLoader; use lang\ast\CompilingClassLoader; use lang\ast\Emitter; +use lang\ast\Language; use lang\ast\Node; use lang\ast\Parse; use lang\ast\Tokens; @@ -29,10 +30,10 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); + $parse= new Parse(Language::named('PHP'), new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); foreach (CompilingClassLoader::$syntax as $syntax) { - $syntax->setup($parse, $emit); + $syntax->setup($parse->language, $emit); } $emit->emit($parse->execute()); diff --git a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php index e6b3790c..600c367c 100755 --- a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php @@ -1,10 +1,11 @@ getName()))->execute(); + return (new Parse(Language::named('php'), new Tokens(new StringTokenizer($code)), $this->getName()))->execute(); } /** From 9cead6204629b3a8af94991832cfaf4bd90d518b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:39:51 +0200 Subject: [PATCH 059/926] Only create language once --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index dd10ab96..7d9a4d21 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -13,11 +13,12 @@ use unittest\TestCase; abstract class EmittingTest extends TestCase { - private static $cl; + private static $cl, $language; private static $id= 0; static function __static() { self::$cl= DynamicClassLoader::instanceFor(self::class); + self::$language= Language::named('PHP'); } /** @@ -30,7 +31,7 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= new Parse(Language::named('PHP'), new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); + $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); foreach (CompilingClassLoader::$syntax as $syntax) { $syntax->setup($parse->language, $emit); From aec97adad1a3578c05871579958c40f37620b6a7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:43:42 +0200 Subject: [PATCH 060/926] Code cleanup --- src/main/php/lang/ast/Compiled.class.php | 4 ++-- src/main/php/lang/ast/CompilingClassloader.class.php | 1 + src/main/php/lang/ast/Parse.class.php | 5 ----- src/main/php/module.xp | 2 -- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 317cac50..f3075b65 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -4,7 +4,7 @@ use text\StreamTokenizer; class Compiled implements OutputStream { - public static $source= [], $emit= []; + public static $source= [], $emit= [], $lang; private $compiled= '', $offset= 0; @@ -15,7 +15,7 @@ public static function bytes($version, $source, $file) { private static function parse($version, $in, $out, $file) { try { - $parse= new Parse(Language::named('php'), new Tokens(new StreamTokenizer($in)), $file); + $parse= new Parse(self::$lang, new Tokens(new StreamTokenizer($in)), $file); $emitter= self::$emit[$version]->newInstance($out); foreach (CompilingClassloader::$syntax as $syntax) { $syntax->setup($parse->language, $emitter); diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 62ba7be0..0338cc7f 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -20,6 +20,7 @@ class CompilingClassLoader implements IClassLoader { private function __construct($emit) { $this->version= $emit->getSimpleName(); Compiled::$emit[$this->version]= $emit; + Compiled::$lang= Language::named('PHP'); stream_wrapper_register($this->version, Compiled::class); } diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 53faa5de..382245c2 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -1,9 +1,6 @@ newInstance(); } - Parse::rules(); - if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); } From 35b7a4a275dd788e99f6e35064723a5d5a871215 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:45:47 +0200 Subject: [PATCH 061/926] Fix trailing comma in parameter list --- src/main/php/lang/ast/language/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/language/PHP.class.php b/src/main/php/lang/ast/language/PHP.class.php index 771196cd..3c6c173a 100755 --- a/src/main/php/lang/ast/language/PHP.class.php +++ b/src/main/php/lang/ast/language/PHP.class.php @@ -149,7 +149,7 @@ public function __construct() { }); $this->infix('(', 80, function($parse, $node, $left) { - $arguments= $this->expressions($parse, ); + $arguments= $this->expressions($parse); $parse->expecting(')', 'invoke expression'); $node->value= new InvokeExpression($left, $arguments); $node->kind= 'invoke'; @@ -310,7 +310,7 @@ public function __construct() { $parse->forward(); $parse->expecting('(', 'new arguments'); - $arguments= $this->expressions($parse, ); + $arguments= $this->expressions($parse); $parse->expecting(')', 'new arguments'); if ('variable' === $type->kind) { From 4b8f7cd7cce71b68211c7e301c4732b470360b64 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:48:41 +0200 Subject: [PATCH 062/926] Remove unused scope argument --- src/main/php/lang/ast/Parse.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 382245c2..12d7271e 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -11,15 +11,16 @@ class Parse { /** * Creates a new parse instance * + * @param lang.ast.Language $language * @param lang.ast.Tokens $tokens * @param string $file * @param lang.ast.Scope $scope */ - public function __construct($language, $tokens, $file= null, $scope= null) { + public function __construct($language, $tokens, $file= null) { $this->language= $language; $this->tokens= $tokens->getIterator(); - $this->scope= $scope ?: new Scope(null); $this->file= $file; + $this->scope= new Scope(null); } /** From 1cb7df6439d990c04c36aa17aba5c2a41146d388 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:50:11 +0200 Subject: [PATCH 063/926] Use uppercase notation for PHP --- src/main/php/lang/ast/Parse.class.php | 1 - src/test/php/lang/ast/unittest/parse/ParseTest.class.php | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 12d7271e..84779fb3 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -14,7 +14,6 @@ class Parse { * @param lang.ast.Language $language * @param lang.ast.Tokens $tokens * @param string $file - * @param lang.ast.Scope $scope */ public function __construct($language, $tokens, $file= null) { $this->language= $language; diff --git a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php index 600c367c..82418e59 100755 --- a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php @@ -6,8 +6,9 @@ use lang\ast\Tokens; use lang\ast\nodes\Value; use text\StringTokenizer; +use unittest\TestCase; -abstract class ParseTest extends \unittest\TestCase { +abstract class ParseTest extends TestCase { /** * Transforms nodes for easy comparison @@ -42,7 +43,7 @@ private function value($arg) { * @return iterable */ protected function parse($code) { - return (new Parse(Language::named('php'), new Tokens(new StringTokenizer($code)), $this->getName()))->execute(); + return (new Parse(Language::named('PHP'), new Tokens(new StringTokenizer($code)), $this->getName()))->execute(); } /** From 3d72feec7ce09cf7f7d18f15c09886f7cbfb9487 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 19:54:01 +0200 Subject: [PATCH 064/926] Use language as variable name to clarify what is being passed --- src/main/php/lang/ast/syntax/NullSafe.class.php | 4 ++-- src/main/php/lang/ast/syntax/TransformationApi.class.php | 2 +- src/main/php/lang/ast/syntax/Using.class.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php index 9796b753..a226d1e7 100755 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -4,8 +4,8 @@ class NullSafe { - public function setup($parser, $emitter) { - $parser->infix('?->', 80, function($parse, $node, $left) { + public function setup($language, $emitter) { + $language->infix('?->', 80, function($parse, $node, $left) { if ('{' === $parse->token->value) { $parse->forward(); $expr= $this->expression($parse, 0); diff --git a/src/main/php/lang/ast/syntax/TransformationApi.class.php b/src/main/php/lang/ast/syntax/TransformationApi.class.php index c22073c0..92097e9e 100755 --- a/src/main/php/lang/ast/syntax/TransformationApi.class.php +++ b/src/main/php/lang/ast/syntax/TransformationApi.class.php @@ -4,7 +4,7 @@ class TransformationApi { - public function setup($parser, $emitter) { + public function setup($language, $emitter) { foreach (Transformations::registered() as $kind => $function) { $emitter->transform($kind, $function); } diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index 4def82a1..29a7e7b6 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -4,8 +4,8 @@ class Using { - public function setup($parser, $emitter) { - $parser->stmt('using', function($parse, $node) { + public function setup($language, $emitter) { + $language->stmt('using', function($parse, $node) { $parse->expecting('(', 'using arguments'); $arguments= $this->expressions($parse, ')'); $parse->expecting(')', 'using arguments'); From 734695f38dbcb60ccedf5bd8b778f24b833f62a1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:07:16 +0200 Subject: [PATCH 065/926] Separate emitter from result --- src/main/php/lang/ast/Compiled.class.php | 6 +- .../lang/ast/CompilingClassloader.class.php | 7 +- src/main/php/lang/ast/Emitter.class.php | 781 +++++++++--------- src/main/php/lang/ast/Result.class.php | 26 + src/main/php/lang/ast/emit/HHVM320.class.php | 8 +- .../ast/emit/OmitConstModifiers.class.php | 8 +- src/main/php/lang/ast/emit/PHP56.class.php | 173 ++-- .../RewriteBlockLambdaExpressions.class.php | 6 +- .../emit/RewriteLambdaExpressions.class.php | 30 +- .../lang/ast/emit/RewriteMultiCatch.class.php | 12 +- .../RewriteNullCoalesceAssignment.class.php | 14 +- .../php/lang/ast/syntax/NullSafe.class.php | 18 +- src/main/php/lang/ast/syntax/Using.class.php | 22 +- .../lang/ast/unittest/EmitterTest.class.php | 14 +- .../ast/unittest/emit/EmittingTest.class.php | 17 +- .../emit/TransformationsTest.class.php | 14 +- 16 files changed, 583 insertions(+), 573 deletions(-) create mode 100755 src/main/php/lang/ast/Result.class.php diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index f3075b65..e3fbb2c4 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -16,11 +16,7 @@ public static function bytes($version, $source, $file) { private static function parse($version, $in, $out, $file) { try { $parse= new Parse(self::$lang, new Tokens(new StreamTokenizer($in)), $file); - $emitter= self::$emit[$version]->newInstance($out); - foreach (CompilingClassloader::$syntax as $syntax) { - $syntax->setup($parse->language, $emitter); - } - $emitter->emit($parse->execute()); + self::$emit[$version]->emit(new Result($out), $parse->execute()); return $out; } finally { $in->close(); diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 0338cc7f..90faa094 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -19,8 +19,13 @@ class CompilingClassLoader implements IClassLoader { /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); - Compiled::$emit[$this->version]= $emit; + Compiled::$emit[$this->version]= $emit->newInstance(); Compiled::$lang= Language::named('PHP'); + + foreach (self::$syntax as $syntax) { + $syntax->setup(Compiled::$lang, Compiled::$emit[$this->version]); + } + stream_wrapper_register($this->version, Compiled::class); } diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 7a52e30c..115f9abe 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -10,13 +10,7 @@ abstract class Emitter { const PROPERTY = 0; const METHOD = 1; - protected $out; - protected $line= 1; - protected $meta= []; protected $unsupported= []; - protected $locals= []; - protected $stack= []; - private $emit= []; /** @@ -38,18 +32,16 @@ public static function forRuntime($runtime) { throw new IllegalArgumentException('XP Compiler does not support '.$runtime.' yet'); } - /** @param io.streams.Writer */ - public function __construct($out) { - $this->out= $out; - $this->id= 0; - } - public function transform($kind, $function) { - $this->emit[$kind]= function($arg) use($function) { - foreach ($function($arg) as $n) { - $this->{'emit'.$n->kind}($n->value); - } - }; + if (null === $function) { + unset($this->emit[$kind]); + } else { + $this->emit[$kind]= function($result, $arg) use($function) { + foreach ($function($arg) as $n) { + $this->{'emit'.$n->kind}($result, $n->value); + } + }; + } return $this; } @@ -58,7 +50,7 @@ public function transform($kind, $function) { * any previous handling, including builtin behavior! * * @param string $kind - * @param function(lang.ast.Node): void $function + * @param function(lang.ast.Result, lang.ast.Node): void $function * @return self */ public function handle($kind, $function) { @@ -66,15 +58,6 @@ public function handle($kind, $function) { return $this; } - /** - * Creates a temporary variable and returns its name - * - * @param string - */ - protected function temp() { - return '$T'.($this->id++); - } - /** * Collects emitted code into a buffer and returns it * @@ -82,15 +65,15 @@ protected function temp() { * @return string */ protected function buffer($callable) { - $o= $this->out; + $o= $result->out; $buffer= new MemoryOutputStream(); - $this->out= new StringWriter($buffer ); + $result->out= new StringWriter($buffer ); try { $callable(); return $buffer->getBytes(); } finally { - $this->out= $o; + $result->out= $o; } } @@ -171,99 +154,99 @@ protected function propertyType($type) { } } - protected function emitStart($start) { - $this->out->write('out->write('out->write('namespace '.$package.";\n"); + protected function emitPackage($result, $package) { + $result->out->write('namespace '.$package.";\n"); } - protected function emitImport($import) { + protected function emitImport($result, $import) { foreach ($import as $type => $alias) { - $this->out->write('use '.$type.($alias ? ' as '.$alias : '').';'); + $result->out->write('use '.$type.($alias ? ' as '.$alias : '').';'); } } - protected function emitImportConst($import) { + protected function emitImportConst($result, $import) { foreach ($import as $type => $alias) { - $this->out->write('use const '.$type.($alias ? ' as '.$alias : '').';'); + $result->out->write('use const '.$type.($alias ? ' as '.$alias : '').';'); } } - protected function emitImportFunction($import) { + protected function emitImportFunction($result, $import) { foreach ($import as $type => $alias) { - $this->out->write('use function '.$type.($alias ? ' as '.$alias : '').';'); + $result->out->write('use function '.$type.($alias ? ' as '.$alias : '').';'); } } - protected function emitAnnotation($annotations) { + protected function emitAnnotation($result, $annotations) { // NOOP } - protected function emitCode($code) { - $this->out->write($code); + protected function emitCode($result, $code) { + $result->out->write($code); } - protected function emitLiteral($literal) { - $this->out->write($literal); + protected function emitLiteral($result, $literal) { + $result->out->write($literal); } - protected function emitName($name) { - $this->out->write($name); + protected function emitName($result, $name) { + $result->out->write($name); } - protected function emitEcho($echo) { - $this->out->write('echo '); + protected function emitEcho($result, $echo) { + $result->out->write('echo '); $s= sizeof($echo) - 1; foreach ($echo as $i => $expr) { - $this->emit($expr); - if ($i < $s) $this->out->write(','); + $this->emit($result, $expr); + if ($i < $s) $result->out->write(','); } } - protected function emitBlock($block) { - $this->out->write('{'); - $this->emit($block); - $this->out->write('}'); + protected function emitBlock($result, $block) { + $result->out->write('{'); + $this->emit($result, $block); + $result->out->write('}'); } - protected function emitStatic($static) { + protected function emitStatic($result, $static) { foreach ($static as $variable => $initial) { - $this->out->write('static $'.$variable); + $result->out->write('static $'.$variable); if ($initial) { - $this->out->write('='); - $this->emit($initial); + $result->out->write('='); + $this->emit($result, $initial); } - $this->out->write(';'); + $result->out->write(';'); } } - protected function emitVariable($variable) { - $this->out->write('$'.$variable); + protected function emitVariable($result, $variable) { + $result->out->write('$'.$variable); } - protected function emitCast($cast) { + protected function emitCast($result, $cast) { static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; $name= $cast->type->name(); if ('?' === $name{0}) { - $this->out->write('cast('); - $this->emit($cast->expression); - $this->out->write(',\''.$name.'\', false)'); + $result->out->write('cast('); + $this->emit($result, $cast->expression); + $result->out->write(',\''.$name.'\', false)'); } else if (isset($native[$name])) { - $this->out->write('('.$cast->type->literal().')'); - $this->emit($cast->expression); + $result->out->write('('.$cast->type->literal().')'); + $this->emit($result, $cast->expression); } else { - $this->out->write('cast('); - $this->emit($cast->expression); - $this->out->write(',\''.$name.'\')'); + $result->out->write('cast('); + $this->emit($result, $cast->expression); + $result->out->write(',\''.$name.'\')'); } } - protected function emitArray($array) { + protected function emitArray($result, $array) { if (empty($array)) { - $this->out->write('[]'); + $result->out->write('[]'); return; } @@ -276,226 +259,226 @@ protected function emitArray($array) { } if ($unpack) { - $this->out->write('array_merge(['); + $result->out->write('array_merge(['); foreach ($array as $pair) { if ($pair[0]) { - $this->emit($pair[0]); - $this->out->write('=>'); + $this->emit($result, $pair[0]); + $result->out->write('=>'); } if ('unpack' === $pair[1]->kind) { if ('array' === $pair[1]->value->kind) { - $this->out->write('],'); - $this->emit($pair[1]->value); - $this->out->write(',['); + $result->out->write('],'); + $this->emit($result, $pair[1]->value); + $result->out->write(',['); } else { - $t= $this->temp(); - $this->out->write('],('.$t.'='); - $this->emit($pair[1]->value); - $this->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); + $t= $result->temp(); + $result->out->write('],('.$t.'='); + $this->emit($result, $pair[1]->value); + $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); } } else { - $this->emit($pair[1]); - $this->out->write(','); + $this->emit($result, $pair[1]); + $result->out->write(','); } } - $this->out->write('])'); + $result->out->write('])'); } else { - $this->out->write('['); + $result->out->write('['); foreach ($array as $pair) { if ($pair[0]) { - $this->emit($pair[0]); - $this->out->write('=>'); + $this->emit($result, $pair[0]); + $result->out->write('=>'); } - $this->emit($pair[1]); - $this->out->write(','); + $this->emit($result, $pair[1]); + $result->out->write(','); } - $this->out->write(']'); + $result->out->write(']'); } } - protected function emitParameter($parameter) { + protected function emitParameter($result, $parameter) { if ($parameter->type && $t= $this->paramType($parameter->type)) { - $this->out->write($t.' '); + $result->out->write($t.' '); } if ($parameter->variadic) { - $this->out->write('... $'.$parameter->name); + $result->out->write('... $'.$parameter->name); } else { - $this->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); + $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); } if ($parameter->default) { - $this->out->write('='); - $this->emit($parameter->default); + $result->out->write('='); + $this->emit($result, $parameter->default); } - $this->locals[$parameter->name]= true; + $result->locals[$parameter->name]= true; } - protected function emitSignature($signature) { - $this->out->write('('); + protected function emitSignature($result, $signature) { + $result->out->write('('); $s= sizeof($signature->parameters) - 1; foreach ($signature->parameters as $i => $parameter) { - $this->emitParameter($parameter); - if ($i < $s) $this->out->write(', '); + $this->emitParameter($result, $parameter); + if ($i < $s) $result->out->write(', '); } - $this->out->write(')'); + $result->out->write(')'); if ($signature->returns && $t= $this->returnType($signature->returns)) { - $this->out->write(':'.$t); + $result->out->write(':'.$t); } } - protected function emitFunction($function) { - $this->stack[]= $this->locals; - $this->locals= []; + protected function emitFunction($result, $function) { + $result->stack[]= $result->locals; + $result->locals= []; - $this->out->write('function '.$function->name); - $this->emitSignature($function->signature); + $result->out->write('function '.$function->name); + $this->emitSignature($result, $function->signature); - $this->out->write('{'); - $this->emit($function->body); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $function->body); + $result->out->write('}'); - $this->locals= array_pop($this->stack); + $result->locals= array_pop($result->stack); } - protected function emitClosure($closure) { - $this->stack[]= $this->locals; - $this->locals= []; + protected function emitClosure($result, $closure) { + $result->stack[]= $result->locals; + $result->locals= []; - $this->out->write('function'); - $this->emitSignature($closure->signature); + $result->out->write('function'); + $this->emitSignature($result, $closure->signature); if ($closure->use) { - $this->out->write(' use('.implode(',', $closure->use).') '); + $result->out->write(' use('.implode(',', $closure->use).') '); foreach ($closure->use as $variable) { - $this->locals[substr($variable, 1)]= true; + $result->locals[substr($variable, 1)]= true; } } - $this->out->write('{'); - $this->emit($closure->body); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $closure->body); + $result->out->write('}'); - $this->locals= array_pop($this->stack); + $result->locals= array_pop($result->stack); } - protected function emitLambda($lambda) { - $this->out->write('fn'); - $this->emitSignature($lambda->signature); - $this->out->write('=>'); + protected function emitLambda($result, $lambda) { + $result->out->write('fn'); + $this->emitSignature($result, $lambda->signature); + $result->out->write('=>'); if (is_array($lambda->body)) { - $this->out->write('{'); - $this->emit($lambda->body); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $lambda->body); + $result->out->write('}'); } else { - $this->emit($lambda->body); + $this->emit($result, $lambda->body); } } - protected function emitClass($class) { - array_unshift($this->meta, []); + protected function emitClass($result, $class) { + array_unshift($result->meta, []); - $this->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $this->out->write(' extends '.$class->parent); - $class->implements && $this->out->write(' implements '.implode(', ', $class->implements)); - $this->out->write('{'); + $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); + $class->parent && $result->out->write(' extends '.$class->parent); + $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); + $result->out->write('{'); foreach ($class->body as $member) { - $this->emit($member); + $this->emit($result, $member); } - $this->out->write('static function __init() {'); - $this->emitMeta($class->name, $class->annotations, $class->comment); - $this->out->write('}} '.$class->name.'::__init();'); + $result->out->write('static function __init() {'); + $this->emitMeta($result, $class->name, $class->annotations, $class->comment); + $result->out->write('}} '.$class->name.'::__init();'); } - protected function emitAnnotations($annotations) { + protected function emitAnnotations($result, $annotations) { foreach ($annotations as $name => $annotation) { - $this->out->write("'".$name."' => "); + $result->out->write("'".$name."' => "); if ($annotation) { - $this->emit($annotation); - $this->out->write(','); + $this->emit($result, $annotation); + $result->out->write(','); } else { - $this->out->write('null,'); + $result->out->write('null,'); } } } - protected function emitMeta($name, $annotations, $comment) { - $this->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); - $this->out->write('"class" => [DETAIL_ANNOTATIONS => ['); - $this->emitAnnotations($annotations); - $this->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); + protected function emitMeta($result, $name, $annotations, $comment) { + $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); + $this->emitAnnotations($result, $annotations); + $result->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); - foreach (array_shift($this->meta) as $type => $lookup) { - $this->out->write($type.' => ['); + foreach (array_shift($result->meta) as $type => $lookup) { + $result->out->write($type.' => ['); foreach ($lookup as $key => $meta) { - $this->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); - $this->emitAnnotations($meta[DETAIL_ANNOTATIONS]); - $this->out->write('], DETAIL_TARGET_ANNO => ['); + $result->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); + $this->emitAnnotations($result, $meta[DETAIL_ANNOTATIONS]); + $result->out->write('], DETAIL_TARGET_ANNO => ['); foreach ($meta[DETAIL_TARGET_ANNO] as $target => $annotations) { - $this->out->write("'$".$target."' => ["); - $this->emitAnnotations($annotations); - $this->out->write('],'); + $result->out->write("'$".$target."' => ["); + $this->emitAnnotations($result, $annotations); + $result->out->write('],'); } - $this->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); - $this->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); - $this->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); + $result->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); + $result->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); + $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); } - $this->out->write('],'); + $result->out->write('],'); } - $this->out->write('];'); + $result->out->write('];'); } - protected function emitInterface($interface) { - array_unshift($this->meta, []); + protected function emitInterface($result, $interface) { + array_unshift($result->meta, []); - $this->out->write('interface '.$this->declaration($interface->name)); - $interface->parents && $this->out->write(' extends '.implode(', ', $interface->parents)); - $this->out->write('{'); + $result->out->write('interface '.$this->declaration($interface->name)); + $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); + $result->out->write('{'); foreach ($interface->body as $member) { - $this->emit($member); - $this->out->write("\n"); + $this->emit($result, $member); + $result->out->write("\n"); } - $this->out->write('}'); + $result->out->write('}'); - $this->emitMeta($interface->name, $interface->annotations, $interface->comment); + $this->emitMeta($result, $interface->name, $interface->annotations, $interface->comment); } - protected function emitTrait($trait) { - array_unshift($this->meta, []); + protected function emitTrait($result, $trait) { + array_unshift($result->meta, []); - $this->out->write('trait '.$this->declaration($trait->name)); - $this->out->write('{'); + $result->out->write('trait '.$this->declaration($trait->name)); + $result->out->write('{'); foreach ($trait->body as $member) { - $this->emit($member); - $this->out->write("\n"); + $this->emit($result, $member); + $result->out->write("\n"); } - $this->out->write('static function __init() {'); - $this->emitMeta($trait->name, $trait->annotations, $trait->comment); - $this->out->write('}} '.$trait->name.'::__init();'); + $result->out->write('static function __init() {'); + $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); + $result->out->write('}} '.$trait->name.'::__init();'); } - protected function emitUse($use) { - $this->out->write('use '.implode(',', $use->types)); + protected function emitUse($result, $use) { + $result->out->write('use '.implode(',', $use->types)); if ($use->aliases) { - $this->out->write('{'); + $result->out->write('{'); foreach ($use->aliases as $reference => $alias) { - $this->out->write($reference.' as '.$alias.';'); + $result->out->write($reference.' as '.$alias.';'); } - $this->out->write('}'); + $result->out->write('}'); } else { - $this->out->write(';'); + $result->out->write(';'); } } - protected function emitConst($const) { - $this->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); - $this->emit($const->expression); - $this->out->write(';'); + protected function emitConst($result, $const) { + $result->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); + $this->emit($result, $const->expression); + $result->out->write(';'); } - protected function emitProperty($property) { - $this->meta[0][self::PROPERTY][$property->name]= [ + protected function emitProperty($result, $property) { + $result->meta[0][self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations ? $property->annotations : [], DETAIL_COMMENT => $property->comment, @@ -503,17 +486,17 @@ protected function emitProperty($property) { DETAIL_ARGUMENTS => [] ]; - $this->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); + $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); if (isset($property->expression)) { - $this->out->write('='); - $this->emit($property->expression); + $result->out->write('='); + $this->emit($result, $property->expression); } - $this->out->write(';'); + $result->out->write(';'); } - protected function emitMethod($method) { - $this->stack[]= $this->locals; - $this->locals= ['this' => true]; + protected function emitMethod($result, $method) { + $result->stack[]= $result->locals; + $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], @@ -527,7 +510,7 @@ protected function emitMethod($method) { if (isset($param->promote)) { $declare.= $param->promote.' $'.$param->name.';'; $promote.= '$this->'.$param->name.'= $'.$param->name.';'; - $this->meta[0][self::PROPERTY][$param->name]= [ + $result->meta[0][self::PROPERTY][$param->name]= [ DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', DETAIL_ANNOTATIONS => [], DETAIL_COMMENT => null, @@ -538,335 +521,335 @@ protected function emitMethod($method) { $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } - $this->out->write($declare); - $this->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $this->emitSignature($method->signature); + $result->out->write($declare); + $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); + $this->emitSignature($result, $method->signature); if (null === $method->body) { - $this->out->write(';'); + $result->out->write(';'); } else { - $this->out->write(' {'.$promote); - $this->emit($method->body); - $this->out->write('}'); + $result->out->write(' {'.$promote); + $this->emit($result, $method->body); + $result->out->write('}'); } - $this->meta[0][self::METHOD][$method->name]= $meta; - $this->locals= array_pop($this->stack); + $result->meta[0][self::METHOD][$method->name]= $meta; + $result->locals= array_pop($result->stack); } - protected function emitBraced($braced) { - $this->out->write('('); - $this->emit($braced); - $this->out->write(')'); + protected function emitBraced($result, $braced) { + $result->out->write('('); + $this->emit($result, $braced); + $result->out->write(')'); } - protected function emitBinary($binary) { - $this->emit($binary->left); - $this->out->write(' '.$binary->operator.' '); - $this->emit($binary->right); + protected function emitBinary($result, $binary) { + $this->emit($result, $binary->left); + $result->out->write(' '.$binary->operator.' '); + $this->emit($result, $binary->right); } - protected function emitUnary($unary) { - $this->out->write($unary->operator); - $this->emit($unary->expression); + protected function emitUnary($result, $unary) { + $result->out->write($unary->operator); + $this->emit($result, $unary->expression); } - protected function emitTernary($ternary) { - $this->emit($ternary->condition); - $this->out->write('?'); - $this->emit($ternary->expression); - $this->out->write(':'); - $this->emit($ternary->otherwise); + protected function emitTernary($result, $ternary) { + $this->emit($result, $ternary->condition); + $result->out->write('?'); + $this->emit($result, $ternary->expression); + $result->out->write(':'); + $this->emit($result, $ternary->otherwise); } - protected function emitOffset($offset) { - $this->emit($offset->expression); + protected function emitOffset($result, $offset) { + $this->emit($result, $offset->expression); if (null === $offset->offset) { - $this->out->write('[]'); + $result->out->write('[]'); } else { - $this->out->write('['); - $this->emit($offset->offset); - $this->out->write(']'); + $result->out->write('['); + $this->emit($result, $offset->offset); + $result->out->write(']'); } } - protected function emitAssign($target) { + protected function emitAssign($result, $target) { if ('variable' === $target->kind) { - $this->out->write('$'.$target->value); - $this->locals[$target->value]= true; + $result->out->write('$'.$target->value); + $result->locals[$target->value]= true; } else if ('array' === $target->kind) { - $this->out->write('list('); + $result->out->write('list('); foreach ($target->value as $pair) { - $this->emitAssign($pair[1]); - $this->out->write(','); + $this->emitAssign($result, $pair[1]); + $result->out->write(','); } - $this->out->write(')'); + $result->out->write(')'); } else { - $this->emit($target); + $this->emit($result, $target); } } - protected function emitAssignment($assignment) { - $this->emitAssign($assignment->variable); - $this->out->write($assignment->operator); - $this->emit($assignment->expression); + protected function emitAssignment($result, $assignment) { + $this->emitAssign($result, $assignment->variable); + $result->out->write($assignment->operator); + $this->emit($result, $assignment->expression); } - protected function emitReturn($return) { - $this->out->write('return '); - $return && $this->emit($return); - $this->out->write(';'); + protected function emitReturn($result, $return) { + $result->out->write('return '); + $return && $this->emit($result, $return); + $result->out->write(';'); } - protected function emitIf($if) { - $this->out->write('if ('); - $this->emit($if->expression); - $this->out->write(') {'); - $this->emit($if->body); - $this->out->write('}'); + protected function emitIf($result, $if) { + $result->out->write('if ('); + $this->emit($result, $if->expression); + $result->out->write(') {'); + $this->emit($result, $if->body); + $result->out->write('}'); if ($if->otherwise) { - $this->out->write('else {'); - $this->emit($if->otherwise); - $this->out->write('}'); + $result->out->write('else {'); + $this->emit($result, $if->otherwise); + $result->out->write('}'); } } - protected function emitSwitch($switch) { - $this->out->write('switch ('); - $this->emit($switch->expression); - $this->out->write(') {'); + protected function emitSwitch($result, $switch) { + $result->out->write('switch ('); + $this->emit($result, $switch->expression); + $result->out->write(') {'); foreach ($switch->cases as $case) { if ($case->expression) { - $this->out->write('case '); - $this->emit($case->expression); - $this->out->write(':'); + $result->out->write('case '); + $this->emit($result, $case->expression); + $result->out->write(':'); } else { - $this->out->write('default:'); + $result->out->write('default:'); } - $this->emit($case->body); + $this->emit($result, $case->body); } - $this->out->write('}'); + $result->out->write('}'); } - protected function emitCatch($catch) { + protected function emitCatch($result, $catch) { if (empty($catch->types)) { - $this->out->write('catch(\\Throwable $'.$catch->variable.') {'); + $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); } else { - $this->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); + $result->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); } - $this->emit($catch->body); - $this->out->write('}'); + $this->emit($result, $catch->body); + $result->out->write('}'); } - protected function emitTry($try) { - $this->out->write('try {'); - $this->emit($try->body); - $this->out->write('}'); + protected function emitTry($result, $try) { + $result->out->write('try {'); + $this->emit($result, $try->body); + $result->out->write('}'); if (isset($try->catches)) { foreach ($try->catches as $catch) { - $this->emitCatch($catch); + $this->emitCatch($result, $catch); } } if (isset($try->finally)) { - $this->out->write('finally {'); - $this->emit($try->finally); - $this->out->write('}'); + $result->out->write('finally {'); + $this->emit($result, $try->finally); + $result->out->write('}'); } } - protected function emitThrow($throw) { - $this->out->write('throw '); - $this->emit($throw); - $this->out->write(';'); + protected function emitThrow($result, $throw) { + $result->out->write('throw '); + $this->emit($result, $throw); + $result->out->write(';'); } - protected function emitThrowExpression($throw) { + protected function emitThrowExpression($result, $throw) { $capture= []; foreach ($this->search($throw, 'variable') as $var) { - if (isset($this->locals[$var->value])) { + if (isset($result->locals[$var->value])) { $capture[$var->value]= true; } } unset($capture['this']); - $this->out->write('(function()'); - $capture && $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $this->out->write('{ throw '); - $this->emit($throw); - $this->out->write('; })()'); + $result->out->write('(function()'); + $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); + $result->out->write('{ throw '); + $this->emit($result, $throw); + $result->out->write('; })()'); } - protected function emitForeach($foreach) { - $this->out->write('foreach ('); - $this->emit($foreach->expression); - $this->out->write(' as '); + protected function emitForeach($result, $foreach) { + $result->out->write('foreach ('); + $this->emit($result, $foreach->expression); + $result->out->write(' as '); if ($foreach->key) { - $this->emit($foreach->key); - $this->out->write(' => '); + $this->emit($result, $foreach->key); + $result->out->write(' => '); } - $this->emit($foreach->value); - $this->out->write(') {'); - $this->emit($foreach->body); - $this->out->write('}'); + $this->emit($result, $foreach->value); + $result->out->write(') {'); + $this->emit($result, $foreach->body); + $result->out->write('}'); } - protected function emitFor($for) { - $this->out->write('for ('); - $this->emitArguments($for->initialization); - $this->out->write(';'); - $this->emitArguments($for->condition); - $this->out->write(';'); - $this->emitArguments($for->loop); - $this->out->write(') {'); - $this->emit($for->body); - $this->out->write('}'); + protected function emitFor($result, $for) { + $result->out->write('for ('); + $this->emitArguments($result, $for->initialization); + $result->out->write(';'); + $this->emitArguments($result, $for->condition); + $result->out->write(';'); + $this->emitArguments($result, $for->loop); + $result->out->write(') {'); + $this->emit($result, $for->body); + $result->out->write('}'); } - protected function emitDo($do) { - $this->out->write('do'); - $this->out->write('{'); - $this->emit($do->body); - $this->out->write('} while ('); - $this->emit($do->expression); - $this->out->write(');'); + protected function emitDo($result, $do) { + $result->out->write('do'); + $result->out->write('{'); + $this->emit($result, $do->body); + $result->out->write('} while ('); + $this->emit($result, $do->expression); + $result->out->write(');'); } - protected function emitWhile($while) { - $this->out->write('while ('); - $this->emit($while->expression); - $this->out->write(') {'); - $this->emit($while->body); - $this->out->write('}'); + protected function emitWhile($result, $while) { + $result->out->write('while ('); + $this->emit($result, $while->expression); + $result->out->write(') {'); + $this->emit($result, $while->body); + $result->out->write('}'); } - protected function emitBreak($break) { - $this->out->write('break '); - $break && $this->emit($break); - $this->out->write(';'); + protected function emitBreak($result, $break) { + $result->out->write('break '); + $break && $this->emit($result, $break); + $result->out->write(';'); } - protected function emitContinue($continue) { - $this->out->write('continue '); - $continue && $this->emit($continue); - $this->out->write(';'); + protected function emitContinue($result, $continue) { + $result->out->write('continue '); + $continue && $this->emit($result, $continue); + $result->out->write(';'); } - protected function emitLabel($label) { - $this->out->write($label.':'); + protected function emitLabel($result, $label) { + $result->out->write($label.':'); } - protected function emitGoto($goto) { - $this->out->write('goto '.$goto); + protected function emitGoto($result, $goto) { + $result->out->write('goto '.$goto); } - protected function emitInstanceOf($instanceof) { - $this->emit($instanceof->expression); - $this->out->write(' instanceof '); + protected function emitInstanceOf($result, $instanceof) { + $this->emit($result, $instanceof->expression); + $result->out->write(' instanceof '); if ($instanceof->type instanceof Node) { - $this->emit($instanceof->type); + $this->emit($result, $instanceof->type); } else { - $this->out->write($instanceof->type); + $result->out->write($instanceof->type); } } - protected function emitArguments($arguments) { + protected function emitArguments($result, $arguments) { $s= sizeof($arguments) - 1; foreach ($arguments as $i => $argument) { - $this->emit($argument); - if ($i < $s) $this->out->write(', '); + $this->emit($result, $argument); + if ($i < $s) $result->out->write(', '); } } - protected function emitNew($new) { - $this->out->write('new '.$new->type.'('); - $this->emitArguments($new->arguments); - $this->out->write(')'); + protected function emitNew($result, $new) { + $result->out->write('new '.$new->type.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); } - protected function emitNewClass($new) { - $this->out->write('new class('); - $this->emitArguments($new->arguments); - $this->out->write(')'); + protected function emitNewClass($result, $new) { + $result->out->write('new class('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); - $new->definition->parent && $this->out->write(' extends '.$new->definition->parent); - $new->definition->implements && $this->out->write(' implements '.implode(', ', $new->definition->implements)); - $this->out->write('{'); + $new->definition->parent && $result->out->write(' extends '.$new->definition->parent); + $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); + $result->out->write('{'); foreach ($new->definition->body as $member) { - $this->emit($member); - $this->out->write("\n"); + $this->emit($result, $member); + $result->out->write("\n"); } - $this->out->write('}'); + $result->out->write('}'); } - protected function emitInvoke($invoke) { - $this->emit($invoke->expression); - $this->out->write('('); - $this->emitArguments($invoke->arguments); - $this->out->write(')'); + protected function emitInvoke($result, $invoke) { + $this->emit($result, $invoke->expression); + $result->out->write('('); + $this->emitArguments($result, $invoke->arguments); + $result->out->write(')'); } - protected function emitScope($scope) { - $this->out->write($scope->type.'::'); - $this->emit($scope->member); + protected function emitScope($result, $scope) { + $result->out->write($scope->type.'::'); + $this->emit($result, $scope->member); } - protected function emitInstance($instance) { + protected function emitInstance($result, $instance) { if ('new' === $instance->expression->kind) { - $this->out->write('('); - $this->emit($instance->expression); - $this->out->write(')->'); + $result->out->write('('); + $this->emit($result, $instance->expression); + $result->out->write(')->'); } else { - $this->emit($instance->expression); - $this->out->write('->'); + $this->emit($result, $instance->expression); + $result->out->write('->'); } if ('name' === $instance->member->kind) { - $this->out->write($instance->member->value); + $result->out->write($instance->member->value); } else { - $this->out->write('{'); - $this->emit($instance->member); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $instance->member); + $result->out->write('}'); } } - protected function emitUnpack($unpack) { - $this->out->write('...'); - $this->emit($unpack); + protected function emitUnpack($result, $unpack) { + $result->out->write('...'); + $this->emit($result, $unpack); } - protected function emitYield($yield) { - $this->out->write('yield '); + protected function emitYield($result, $yield) { + $result->out->write('yield '); if ($yield->key) { - $this->emit($yield->key); - $this->out->write('=>'); + $this->emit($result, $yield->key); + $result->out->write('=>'); } if ($yield->value) { - $this->emit($yield->value); + $this->emit($result, $yield->value); } } - protected function emitFrom($from) { - $this->out->write('yield from '); - $this->emit($from); + protected function emitFrom($result, $from) { + $result->out->write('yield from '); + $this->emit($result, $from); } - public function emit($arg) { + public function emit($result, $arg) { if ($arg instanceof Element) { - if ($arg->line > $this->line) { - $this->out->write(str_repeat("\n", $arg->line - $this->line)); - $this->line= $arg->line; + if ($arg->line > $result->line) { + $result->out->write(str_repeat("\n", $arg->line - $result->line)); + $result->line= $arg->line; } if (isset($this->emit[$arg->kind])) { - $this->emit[$arg->kind]($arg); + $this->emit[$arg->kind]($result, $arg); } else { - $this->{'emit'.$arg->kind}($arg->value); + $this->{'emit'.$arg->kind}($result, $arg->value); } } else { foreach ($arg as $node) { - $this->emit($node); - isset($node->symbol->std) || $this->out->write(';'); + $this->emit($result, $node); + isset($node->symbol->std) || $result->out->write(';'); } } } diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php new file mode 100755 index 00000000..f1aee304 --- /dev/null +++ b/src/main/php/lang/ast/Result.class.php @@ -0,0 +1,26 @@ +out= $out; + } + + /** + * Creates a temporary variable and returns its name + * + * @return string + */ + public function temp() { + return '$T'.($this->id++); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/HHVM320.class.php b/src/main/php/lang/ast/emit/HHVM320.class.php index f6ddcec3..d83dea44 100755 --- a/src/main/php/lang/ast/emit/HHVM320.class.php +++ b/src/main/php/lang/ast/emit/HHVM320.class.php @@ -17,12 +17,12 @@ class HHVM320 extends Emitter { 'mixed' => null, ]; - protected function emitParameter($parameter) { + protected function emitParameter($result, $parameter) { if ($parameter->variadic) { - $this->out->write('... $'.$parameter->name); - $this->locals[$parameter->name]= true; + $result->out->write('... $'.$parameter->name); + $result->locals[$parameter->name]= true; } else { - parent::emitParameter($parameter); + parent::emitParameter($result$parameter); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php index 882223a6..8cb8f2a8 100755 --- a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php +++ b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php @@ -8,9 +8,9 @@ */ trait OmitConstModifiers { - protected function emitConst($const) { - $this->out->write('const '.$const->name.'='); - $this->emit($const->expression); - $this->out->write(';'); + protected function emitConst($result, $const) { + $result->out->write('const '.$const->name.'='); + $this->emit($result, $const->expression); + $result->out->write(';'); } } diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 6f6e99c7..37557276 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -22,7 +22,6 @@ class PHP56 extends Emitter { 'float' => 70, 'mixed' => null, ]; - private $call= []; private static $keywords= [ 'callable' => true, 'class' => true, @@ -91,176 +90,176 @@ class PHP56 extends Emitter { ]; - protected function emitLiteral($literal) { + protected function emitLiteral($result, $literal) { if ('"' === $literal{0}) { - $this->out->write(preg_replace_callback( + $result->out->write(preg_replace_callback( '/\\\\u\{([0-9a-f]+)\}/i', function($matches) { return html_entity_decode('&#'.hexdec($matches[1]).';', ENT_HTML5, \xp::ENCODING); }, $literal )); } else { - $this->out->write($literal); + $result->out->write($literal); } } - protected function emitCatch($catch) { + protected function emitCatch($result, $catch) { if (empty($catch->types)) { - $this->out->write('catch(\\Exception $'.$catch->variable.') {'); + $result->out->write('catch(\\Exception $'.$catch->variable.') {'); } else { $last= array_pop($catch->types); $label= sprintf('c%u', crc32($last)); foreach ($catch->types as $type) { - $this->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); + $result->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); } - $this->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); + $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); } - $this->emit($catch->body); - $this->out->write('}'); + $this->emit($result, $catch->body); + $result->out->write('}'); } - protected function emitBinary($binary) { + protected function emitBinary($result, $binary) { if ('??' === $binary->operator) { - $this->out->write('isset('); - $this->emit($binary->left); - $this->out->write(') ?'); - $this->emit($binary->left); - $this->out->write(' : '); - $this->emit($binary->right); + $result->out->write('isset('); + $this->emit($result, $binary->left); + $result->out->write(') ?'); + $this->emit($result, $binary->left); + $result->out->write(' : '); + $this->emit($result, $binary->right); } else if ('<=>' === $binary->operator) { - $l= $this->temp(); - $r= $this->temp(); - $this->out->write('('.$l.'= '); - $this->emit($binary->left); - $this->out->write(') < ('.$r.'='); - $this->emit($binary->right); - $this->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); + $l= $result->temp(); + $r= $result->temp(); + $result->out->write('('.$l.'= '); + $this->emit($result, $binary->left); + $result->out->write(') < ('.$r.'='); + $this->emit($result, $binary->right); + $result->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); } else { parent::emitBinary($binary); } } - protected function emitAssignment($assignment) { + protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { - $this->out->write('isset('); + $result->out->write('isset('); $this->emitAssign($assignment->variable); - $this->out->write(') ||'); - $this->emit($assignment->variable); - $this->out->write('='); - $this->emit($assignment->expression); + $result->out->write(') ||'); + $this->emit($result, $assignment->variable); + $result->out->write('='); + $this->emit($result, $assignment->expression); } else { $this->emitAssign($assignment->variable); - $this->out->write($assignment->operator); - $this->emit($assignment->expression); + $result->out->write($assignment->operator); + $this->emit($result, $assignment->expression); } } /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitInvoke($invoke) { + protected function emitInvoke($result, $invoke) { $expr= $invoke->expression; if ('braced' === $expr->kind) { - $t= $this->temp(); - $this->out->write('(('.$t.'='); - $this->emit($expr->value); - $this->out->write(') ? '.$t); - $this->out->write('('); + $t= $result->temp(); + $result->out->write('(('.$t.'='); + $this->emit($result, $expr->value); + $result->out->write(') ? '.$t); + $result->out->write('('); $this->emitArguments($invoke->arguments); - $this->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); + $result->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); } else if ( 'scope' === $expr->kind && 'name' === $expr->value->member->kind && isset(self::$keywords[strtolower($expr->value->member->value)]) ) { - $this->out->write($expr->value->type.'::{\''.$expr->value->member->value.'\'}'); - $this->out->write('('); + $result->out->write($expr->value->type.'::{\''.$expr->value->member->value.'\'}'); + $result->out->write('('); $this->emitArguments($invoke->arguments); - $this->out->write(')'); + $result->out->write(')'); } else { parent::emitInvoke($invoke); } } /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitThrowExpression($throw) { + protected function emitThrowExpression($result, $throw) { $capture= []; foreach ($this->search($throw, 'variable') as $var) { - if (isset($this->locals[$var->value])) { + if (isset($result->locals[$var->value])) { $capture[$var->value]= true; } } unset($capture['this']); - $t= $this->temp(); - $this->out->write('(('.$t.'=function()'); - $capture && $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $this->out->write('{ throw '); - $this->emit($throw); - $this->out->write('; }) ? '.$t.'() : null)'); + $t= $result->temp(); + $result->out->write('(('.$t.'=function()'); + $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); + $result->out->write('{ throw '); + $this->emit($result, $throw); + $result->out->write('; }) ? '.$t.'() : null)'); } - protected function emitNewClass($new) { - $this->out->write('\\lang\\ClassLoader::defineType("class©anonymous'.md5(uniqid()).'", ["kind" => "class"'); + protected function emitNewClass($result, $new) { + $result->out->write('\\lang\\ClassLoader::defineType("class©anonymous'.md5(uniqid()).'", ["kind" => "class"'); $definition= $new->definition; - $this->out->write(', "extends" => '.($definition->parent ? '[\''.$definition->parent.'\']' : 'null')); - $this->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); - $this->out->write(', "use" => []'); - $this->out->write('], \'{'); - $this->out->write(str_replace('\'', '\\\'', $this->buffer(function() use($definition) { + $result->out->write(', "extends" => '.($definition->parent ? '[\''.$definition->parent.'\']' : 'null')); + $result->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); + $result->out->write(', "use" => []'); + $result->out->write('], \'{'); + $result->out->write(str_replace('\'', '\\\'', $this->buffer(function() use($definition) { foreach ($definition->body as $member) { - $this->emit($member); - $this->out->write("\n"); + $this->emit($result, $member); + $result->out->write("\n"); } }))); - $this->out->write('}\')->newInstance('); - $this->emitArguments($new->arguments); - $this->out->write(')'); + $result->out->write('}\')->newInstance('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); } - protected function emitFrom($from) { - $this->out->write('foreach ('); - $this->emit($from); - $this->out->write(' as $key => $val) yield $key => $val;'); + protected function emitFrom($result, $from) { + $result->out->write('foreach ('); + $this->emit($result, $from); + $result->out->write(' as $key => $val) yield $key => $val;'); } /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitMethod($method) { + protected function emitMethod($result, $method) { if (isset(self::$keywords[strtolower($method->name)])) { - $this->call[in_array('static', $method->modifiers)][]= $method->name; + $result->call[in_array('static', $method->modifiers)][]= $method->name; $method->name= '__'.$method->name; } else if ('__call' === $method->name || '__callStatic' === $method->name) { $method->name.= '0'; } - parent::emitMethod($method); + parent::emitMethod($result, $method); } - protected function emitClass($class) { - $this->call= [false => [], true => []]; + protected function emitClass($result, $class) { + $result->call= [false => [], true => []]; array_unshift($this->meta, []); - $this->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $this->out->write(' extends '.$class->parent); - $class->implements && $this->out->write(' implements '.implode(', ', $class->implements)); - $this->out->write('{'); + $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); + $class->parent && $result->out->write(' extends '.$class->parent); + $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); + $result->out->write('{'); foreach ($class->body as $member) { - $this->emit($member); + $this->emit($result, $member); } - if ($this->call[false]) { - $this->out->write('function __call($name, $args) {'); + if ($result->call[false]) { + $result->out->write('function __call($name, $args) {'); foreach ($this->call[false] as $name) { - $this->out->write('if (\''.$name.'\' === $name) return $this->__'.$name.'(...$args); else '); + $result->out->write('if (\''.$name.'\' === $name) return $this->__'.$name.'(...$args); else '); } - $this->out->write('return $this->__call0($name, $args); }'); + $result->out->write('return $this->__call0($name, $args); }'); } - if ($this->call[true]) { - $this->out->write('static function __callStatic($name, $args) {'); + if ($result->call[true]) { + $result->out->write('static function __callStatic($name, $args) {'); foreach ($this->call[true] as $name) { - $this->out->write('if (\''.$name.'\' === $name) return self::__'.$name.'(...$args); else '); + $result->out->write('if (\''.$name.'\' === $name) return self::__'.$name.'(...$args); else '); } - $this->out->write('return self::__callStatic0($name, ...$args); }'); + $result->out->write('return self::__callStatic0($name, ...$args); }'); } - $this->out->write('static function __init() {'); - $this->emitMeta($class->name, $class->annotations, $class->comment); - $this->out->write('}} '.$class->name.'::__init();'); + $result->out->write('static function __init() {'); + $this->emitMeta($result, $class->name, $class->annotations, $class->comment); + $result->out->write('}} '.$class->name.'::__init();'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 9b42223c..407b84c2 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -9,11 +9,11 @@ trait RewriteBlockLambdaExpressions { use RewriteLambdaExpressions { emitLambda as rewriteLambda; } - protected function emitLambda($lambda) { + protected function emitLambda($result, $lambda) { if (is_array($lambda->body)) { - $this->rewriteLambda($lambda); + $this->rewriteLambda($result, $lambda); } else { - parent::emitLambda($lambda); + parent::emitLambda($result, $lambda); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index f58c43a7..613e3ff8 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -7,41 +7,41 @@ */ trait RewriteLambdaExpressions { - protected function emitLambda($lambda) { + protected function emitLambda($result, $lambda) { $capture= []; foreach ($this->search($lambda->body, 'variable') as $var) { - if (isset($this->locals[$var->value])) { + if (isset($result->locals[$var->value])) { $capture[$var->value]= true; } } unset($capture['this']); - $this->stack[]= $this->locals; - $this->locals= []; + $result->stack[]= $result->locals; + $result->locals= []; - $this->out->write('function'); - $this->emitSignature($lambda->signature); + $result->out->write('function'); + $this->emitSignature($result, $lambda->signature); foreach ($lambda->signature->parameters as $param) { unset($capture[$param->name]); } if ($capture) { - $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); + $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); foreach ($capture as $name => $_) { - $this->locals[$name]= true; + $result->locals[$name]= true; } } if (is_array($lambda->body)) { - $this->out->write('{'); - $this->emit($lambda->body); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $lambda->body); + $result->out->write('}'); } else { - $this->out->write('{ return '); - $this->emit($lambda->body); - $this->out->write('; }'); + $result->out->write('{ return '); + $this->emit($result, $lambda->body); + $result->out->write('; }'); } - $this->locals= array_pop($this->stack); + $result->locals= array_pop($result->stack); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php index 082004ce..5ff4f9cc 100755 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php @@ -7,19 +7,19 @@ */ trait RewriteMultiCatch { - protected function emitCatch($catch) { + protected function emitCatch($result, $catch) { if (empty($catch->types)) { - $this->out->write('catch(\\Throwable $'.$catch->variable.') {'); + $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); } else { $last= array_pop($catch->types); $label= sprintf('c%u', crc32($last)); foreach ($catch->types as $type) { - $this->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); + $result->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); } - $this->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); + $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); } - $this->emit($catch->body); - $this->out->write('}'); + $this->emit($result, $catch->body); + $result->out->write('}'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php index 4480c808..d19f341c 100755 --- a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php +++ b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php @@ -8,15 +8,15 @@ */ trait RewriteNullCoalesceAssignment { - protected function emitAssignment($assignment) { + protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { - $this->emitAssign($assignment->variable); - $this->out->write('='); - $this->emit($assignment->variable); - $this->out->write('??'); - $this->emit($assignment->expression); + $this->emitAssign($result, $assignment->variable); + $result->out->write('='); + $this->emit($result, $assignment->variable); + $result->out->write('??'); + $this->emit($result, $assignment->expression); } else { - parent::emitAssignment($assignment); + parent::emitAssignment($result, $assignment); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php index a226d1e7..247bce06 100755 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -19,18 +19,18 @@ public function setup($language, $emitter) { $node->kind= 'nullsafeinstance'; return $node; }); - $emitter->handle('nullsafeinstance', function($instance) { - $t= $this->temp(); - $this->out->write('null === ('.$t.'= '); - $this->emit($instance->value->expression); - $this->out->write(') ? null : '.$t.'->'); + $emitter->handle('nullsafeinstance', function($result, $instance) { + $t= $result->temp(); + $result->out->write('null === ('.$t.'= '); + $this->emit($result, $instance->value->expression); + $result->out->write(') ? null : '.$t.'->'); if ('name' === $instance->value->member->kind) { - $this->out->write($instance->value->member->value); + $result->out->write($instance->value->member->value); } else { - $this->out->write('{'); - $this->emit($instance->value->member); - $this->out->write('}'); + $result->out->write('{'); + $this->emit($result, $instance->value->member); + $result->out->write('}'); } }); } diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index 29a7e7b6..a16fcfd2 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -19,28 +19,28 @@ public function setup($language, $emitter) { return $node; }); - $emitter->handle('using', function($node) { + $emitter->handle('using', function($result, $node) { $variables= []; foreach ($node->value->arguments as $expression) { switch ($expression->kind) { case 'variable': $variables[]= '$'.$expression->value; break; case 'assignment': $variables[]= '$'.$expression->value->variable->value; break; - default: $temp= $this->temp(); $variables[]= $temp; $this->out->write($temp.'='); + default: $temp= $result->temp(); $variables[]= $temp; $result->out->write($temp.'='); } - $this->emit($expression); - $this->out->write(';'); + $this->emit($result, $expression); + $result->out->write(';'); } - $this->out->write('try {'); - $this->emit($node->value->body); + $result->out->write('try {'); + $this->emit($result, $node->value->body); - $this->out->write('} finally {'); + $result->out->write('} finally {'); foreach ($variables as $variable) { - $this->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); - $this->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); - $this->out->write('unset('.$variable.');'); + $result->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); + $result->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); + $result->out->write('unset('.$variable.');'); } - $this->out->write('}'); + $result->out->write('}'); }); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 591d293e..1574df79 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -1,20 +1,12 @@ out= new MemoryOutputStream(); - } +class EmitterTest extends TestCase { #[@test] public function can_create() { - $runtime= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; - Emitter::forRuntime($runtime)->newInstance(new StringWriter($this->out)); + Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 7d9a4d21..4feeb46e 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -8,17 +8,26 @@ use lang\ast\Language; use lang\ast\Node; use lang\ast\Parse; +use lang\ast\Result; use lang\ast\Tokens; use text\StringTokenizer; use unittest\TestCase; abstract class EmittingTest extends TestCase { - private static $cl, $language; + private static $cl, $language, $emitter; private static $id= 0; static function __static() { self::$cl= DynamicClassLoader::instanceFor(self::class); self::$language= Language::named('PHP'); + self::$emitter= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + foreach (CompilingClassLoader::$syntax as $syntax) { + $syntax->setup(self::$language, self::$emitter); + } + } + + protected static function transform($type, $function) { + self::$emitter->transform($type, $function); } /** @@ -32,12 +41,8 @@ protected function type($code) { $out= new MemoryOutputStream(); $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); - $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); - foreach (CompilingClassLoader::$syntax as $syntax) { - $syntax->setup($parse->language, $emit); - } + self::$emitter->emit(new Result(new StringWriter($out)), $parse->execute()); - $emit->emit($parse->execute()); // var_dump($out->getBytes()); self::$cl->setClassBytes($name, $out->getBytes()); return self::$cl->loadClass($name); diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index eaf8976d..f8192130 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -3,13 +3,12 @@ use lang\ast\Code; use lang\ast\nodes\Method; use lang\ast\nodes\Signature; -use lang\ast\transform\Transformations; class TransformationsTest extends EmittingTest { #[@beforeClass] public static function registerTransformation() { - Transformations::register('class', function($class) { + self::transform('class', function($class) { if ($class->value->annotation('getters')) { foreach ($class->value->properties() as $property) { $class->value->inject(new Method( @@ -24,8 +23,13 @@ public static function registerTransformation() { }); } - #[@test] - public function generates_accessor() { + #[@afterClass] + public static function removeTransformation() { + self::transform('class', null); + } + + #[@test, @values(['id', 'name'])] + public function generates_accessor($name) { $t= $this->type('<> class { private int $id; private string $name; @@ -35,6 +39,6 @@ public function __construct(int $id, string $name) { $this->name= $name; } }'); - $this->assertTrue($t->hasMethod('id')); + $this->assertTrue($t->hasMethod($name)); } } \ No newline at end of file From 733688f46ada9bce68add635876aae0f4dcb97c1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:12:42 +0200 Subject: [PATCH 066/926] Fix missing result parameters --- src/main/php/lang/ast/Emitter.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP56.class.php | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 115f9abe..c4050066 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -61,13 +61,14 @@ public function handle($kind, $function) { /** * Collects emitted code into a buffer and returns it * + * @param lang.ast.Result * @param function(): void $callable * @return string */ - protected function buffer($callable) { + protected function buffer($result, $callable) { $o= $result->out; $buffer= new MemoryOutputStream(); - $result->out= new StringWriter($buffer ); + $result->out= new StringWriter($buffer); try { $callable(); diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 37557276..57d63abd 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -135,7 +135,7 @@ protected function emitBinary($result, $binary) { $this->emit($result, $binary->right); $result->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); } else { - parent::emitBinary($binary); + parent::emitBinary($result, $binary); } } @@ -175,7 +175,7 @@ protected function emitInvoke($result, $invoke) { $this->emitArguments($invoke->arguments); $result->out->write(')'); } else { - parent::emitInvoke($invoke); + parent::emitInvoke($result, $invoke); } } @@ -204,7 +204,7 @@ protected function emitNewClass($result, $new) { $result->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); $result->out->write(', "use" => []'); $result->out->write('], \'{'); - $result->out->write(str_replace('\'', '\\\'', $this->buffer(function() use($definition) { + $result->out->write(str_replace('\'', '\\\'', $this->buffer($result, function() use($result, $definition) { foreach ($definition->body as $member) { $this->emit($result, $member); $result->out->write("\n"); @@ -234,7 +234,7 @@ protected function emitMethod($result, $method) { protected function emitClass($result, $class) { $result->call= [false => [], true => []]; - array_unshift($this->meta, []); + array_unshift($result->meta, []); $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); $class->parent && $result->out->write(' extends '.$class->parent); $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); From b6f74f7e2cfc376a75d667190e1e8857fd126a71 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:16:57 +0200 Subject: [PATCH 067/926] Fix emitAssign() and emitArguments() --- src/main/php/lang/ast/emit/PHP56.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 57d63abd..462a835a 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -142,13 +142,13 @@ protected function emitBinary($result, $binary) { protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { $result->out->write('isset('); - $this->emitAssign($assignment->variable); + $this->emitAssign($result, $assignment->variable); $result->out->write(') ||'); $this->emit($result, $assignment->variable); $result->out->write('='); $this->emit($result, $assignment->expression); } else { - $this->emitAssign($assignment->variable); + $this->emitAssign($result, $assignment->variable); $result->out->write($assignment->operator); $this->emit($result, $assignment->expression); } @@ -163,7 +163,7 @@ protected function emitInvoke($result, $invoke) { $this->emit($result, $expr->value); $result->out->write(') ? '.$t); $result->out->write('('); - $this->emitArguments($invoke->arguments); + $this->emitArguments($result, $invoke->arguments); $result->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); } else if ( 'scope' === $expr->kind && @@ -172,7 +172,7 @@ protected function emitInvoke($result, $invoke) { ) { $result->out->write($expr->value->type.'::{\''.$expr->value->member->value.'\'}'); $result->out->write('('); - $this->emitArguments($invoke->arguments); + $this->emitArguments($result, $invoke->arguments); $result->out->write(')'); } else { parent::emitInvoke($result, $invoke); From 2b9053c219422f03b5a2c2c0585c777702daf4aa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:27:35 +0200 Subject: [PATCH 068/926] Fix __call|__callStatic recursion --- src/main/php/lang/ast/emit/PHP56.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 462a835a..f1c58c3a 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -245,14 +245,14 @@ protected function emitClass($result, $class) { if ($result->call[false]) { $result->out->write('function __call($name, $args) {'); - foreach ($this->call[false] as $name) { + foreach ($result->call[false] as $name) { $result->out->write('if (\''.$name.'\' === $name) return $this->__'.$name.'(...$args); else '); } $result->out->write('return $this->__call0($name, $args); }'); } if ($result->call[true]) { $result->out->write('static function __callStatic($name, $args) {'); - foreach ($this->call[true] as $name) { + foreach ($result->call[true] as $name) { $result->out->write('if (\''.$name.'\' === $name) return self::__'.$name.'(...$args); else '); } $result->out->write('return self::__callStatic0($name, ...$args); }'); From 37f1af9eb8b549a06486e6bbd1f64653228baaa7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:32:21 +0200 Subject: [PATCH 069/926] Use local variables for syntax setup --- src/main/php/lang/ast/CompilingClassloader.class.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 90faa094..8decef7e 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -19,13 +19,15 @@ class CompilingClassLoader implements IClassLoader { /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); - Compiled::$emit[$this->version]= $emit->newInstance(); - Compiled::$lang= Language::named('PHP'); + $emitter= $emit->newInstance(); + $language= Language::named('PHP'); foreach (self::$syntax as $syntax) { - $syntax->setup(Compiled::$lang, Compiled::$emit[$this->version]); + $syntax->setup($language, $emitter); } + Compiled::$emit[$this->version]= $emitter; + Compiled::$lang= $language; stream_wrapper_register($this->version, Compiled::class); } From ec99d9c7f2efa8431e63e1490967cb6022c0c73e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:35:34 +0200 Subject: [PATCH 070/926] Move buffer() method to result --- src/main/php/lang/ast/Emitter.class.php | 20 -------------------- src/main/php/lang/ast/Result.class.php | 19 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP56.class.php | 2 +- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index c4050066..7f6e26a5 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -58,26 +58,6 @@ public function handle($kind, $function) { return $this; } - /** - * Collects emitted code into a buffer and returns it - * - * @param lang.ast.Result - * @param function(): void $callable - * @return string - */ - protected function buffer($result, $callable) { - $o= $result->out; - $buffer= new MemoryOutputStream(); - $result->out= new StringWriter($buffer); - - try { - $callable(); - return $buffer->getBytes(); - } finally { - $result->out= $o; - } - } - /** * Returns the simple name for use in a declaration * diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index f1aee304..3af324db 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -23,4 +23,23 @@ public function __construct($out) { public function temp() { return '$T'.($this->id++); } + + /** + * Collects emitted code into a buffer and returns it + * + * @param function(lang.ast.Result): void $callable + * @return string + */ + protected function buffer($callable) { + $out= $this->out; + $buffer= new MemoryOutputStream(); + $this->out= new StringWriter($buffer); + + try { + $callable($this); + return $buffer->getBytes(); + } finally { + $this->out= $out; + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index f1c58c3a..eb4a81f6 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -204,7 +204,7 @@ protected function emitNewClass($result, $new) { $result->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); $result->out->write(', "use" => []'); $result->out->write('], \'{'); - $result->out->write(str_replace('\'', '\\\'', $this->buffer($result, function() use($result, $definition) { + $result->out->write(str_replace('\'', '\\\'', $result->buffer(function($result) use($definition) { foreach ($definition->body as $member) { $this->emit($result, $member); $result->out->write("\n"); From b5b6891e775e063ef7da531c76ca3d8cd0b186aa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:40:05 +0200 Subject: [PATCH 071/926] Make buffer() public --- src/main/php/lang/ast/Result.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 3af324db..7545d742 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -30,7 +30,7 @@ public function temp() { * @param function(lang.ast.Result): void $callable * @return string */ - protected function buffer($callable) { + public function buffer($callable) { $out= $this->out; $buffer= new MemoryOutputStream(); $this->out= new StringWriter($buffer); From 7544d4e87f4d0646507b09fb34703051f30e179d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2019 21:42:39 +0200 Subject: [PATCH 072/926] Add missing imports --- src/main/php/lang/ast/Emitter.class.php | 2 -- src/main/php/lang/ast/Result.class.php | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 7f6e26a5..f0e27b14 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,7 +1,5 @@ Date: Mon, 2 Sep 2019 00:20:48 +0200 Subject: [PATCH 073/926] Fix `xp compile` --- .../php/xp/compiler/CompileRunner.class.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 502dbf18..56d7cdd1 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -7,6 +7,7 @@ use lang\ast\Errors; use lang\ast\Language; use lang\ast\Parse; +use lang\ast\Result; use lang\ast\Tokens; use text\StreamTokenizer; use util\cmd\Console; @@ -66,7 +67,12 @@ public static function main(array $args) { } } - $emit= Emitter::forRuntime($target); + $lang= Language::named('PHP'); + $emit= Emitter::forRuntime($target)->newInstance(); + foreach (CompilingClassloader::$syntax as $syntax) { + $syntax->setup($lang, $emit); + } + $input= Input::newInstance($in); $output= Output::newInstance($out); @@ -77,13 +83,8 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= new Parse(Language::named('PHP'), new Tokens(new StreamTokenizer($in)), $file); - $emitter= $emit->newInstance($output->target((string)$path)); - foreach (CompilingClassloader::$syntax as $syntax) { - $syntax->setup($parse->language, $emitter); - } - - $emitter->emit($parse->execute()); + $parse= new Parse($lang, new Tokens(new StreamTokenizer($in)), $file); + $emit->emit(new Result($output->target((string)$path)), $parse->execute()); $t->stop(); Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); @@ -104,7 +105,7 @@ public static function main(array $args) { $errors ? "\033[41;1;37m×" : "\033[42;1;37m♥", $total, $out, - $emit->getName(), + typeof($emit)->getName(), $errors ); Console::$err->writeLinef( From 5214c5cde0c4b346cfb0bebc423ef61f46f2413a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:10:10 +0200 Subject: [PATCH 074/926] Refactor AST to consist of nodes only See xp-framework/ast#2 --- src/main/php/lang/ast/Emitter.class.php | 115 +++--- src/main/php/lang/ast/Language.class.php | 29 +- src/main/php/lang/ast/language/PHP.class.php | 362 ++++++++----------- 3 files changed, 197 insertions(+), 309 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index f0e27b14..b38a83bc 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -36,7 +36,7 @@ public function transform($kind, $function) { } else { $this->emit[$kind]= function($result, $arg) use($function) { foreach ($function($arg) as $n) { - $this->{'emit'.$n->kind}($result, $n->value); + $this->{'emit'.$n->kind}($result, $n); } }; } @@ -84,30 +84,15 @@ protected function type($name) { /** * Search a given scope recursively for nodes with a given kind * - * @param lang.ast.Node|lang.ast.Node[] $arg + * @param lang.ast.Element $arg * @param string $kind * @return iterable */ protected function search($arg, $kind) { - if ($arg instanceof Node) { // TODO: Do we need this? - if ($arg->kind === $kind) { - yield $arg; - } else { - foreach ($this->search($arg->value, $kind) as $result) { - yield $result; - } - } - } else if ($arg instanceof Value) { // TODO: Move recursion into Kind subclasses - foreach ((array)$arg as $node) { - foreach ($this->search($node, $kind) as $result) { - yield $result; - } - } - } else if (is_array($arg)) { - foreach ($arg as $node) { - foreach ($this->search($node, $kind) as $result) { - yield $result; - } + if ($arg->kind === $kind) yield $arg; + foreach ($arg->children() as $child) { + foreach ($this->search($child, $kind) as $result) { + yield $result; } } } @@ -137,25 +122,13 @@ protected function emitStart($result, $start) { $result->out->write('out->write('namespace '.$package.";\n"); + protected function emitNamespace($result, $declaration) { + $result->out->write('namespace '.$declaration->name.";\n"); } protected function emitImport($result, $import) { - foreach ($import as $type => $alias) { - $result->out->write('use '.$type.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitImportConst($result, $import) { - foreach ($import as $type => $alias) { - $result->out->write('use const '.$type.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitImportFunction($result, $import) { - foreach ($import as $type => $alias) { - $result->out->write('use function '.$type.($alias ? ' as '.$alias : '').';'); + foreach ($import->names as $name => $alias) { + $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';'); } } @@ -164,11 +137,11 @@ protected function emitAnnotation($result, $annotations) { } protected function emitCode($result, $code) { - $result->out->write($code); + $result->out->write($code->value); } protected function emitLiteral($result, $literal) { - $result->out->write($literal); + $result->out->write($literal->expression); } protected function emitName($result, $name) { @@ -177,8 +150,8 @@ protected function emitName($result, $name) { protected function emitEcho($result, $echo) { $result->out->write('echo '); - $s= sizeof($echo) - 1; - foreach ($echo as $i => $expr) { + $s= sizeof($echo->expressions) - 1; + foreach ($echo->expressions as $i => $expr) { $this->emit($result, $expr); if ($i < $s) $result->out->write(','); } @@ -186,12 +159,12 @@ protected function emitEcho($result, $echo) { protected function emitBlock($result, $block) { $result->out->write('{'); - $this->emit($result, $block); + $this->emit($result, $block->statements); $result->out->write('}'); } protected function emitStatic($result, $static) { - foreach ($static as $variable => $initial) { + foreach ($static->initializations as $variable => $initial) { $result->out->write('static $'.$variable); if ($initial) { $result->out->write('='); @@ -202,7 +175,7 @@ protected function emitStatic($result, $static) { } protected function emitVariable($result, $variable) { - $result->out->write('$'.$variable); + $result->out->write('$'.$variable->name); } protected function emitCast($result, $cast) { @@ -224,13 +197,13 @@ protected function emitCast($result, $cast) { } protected function emitArray($result, $array) { - if (empty($array)) { + if (empty($array->values)) { $result->out->write('[]'); return; } $unpack= false; - foreach ($array as $pair) { + foreach ($array->values as $pair) { if ('unpack' === $pair[1]->kind) { $unpack= true; break; @@ -239,20 +212,20 @@ protected function emitArray($result, $array) { if ($unpack) { $result->out->write('array_merge(['); - foreach ($array as $pair) { + foreach ($array->values as $pair) { if ($pair[0]) { $this->emit($result, $pair[0]); $result->out->write('=>'); } if ('unpack' === $pair[1]->kind) { - if ('array' === $pair[1]->value->kind) { + if ('array' === $pair[1]->expression->kind) { $result->out->write('],'); - $this->emit($result, $pair[1]->value); + $this->emit($result, $pair[1]->expression); $result->out->write(',['); } else { $t= $result->temp(); $result->out->write('],('.$t.'='); - $this->emit($result, $pair[1]->value); + $this->emit($result, $pair[1]->expression); $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); } } else { @@ -263,7 +236,7 @@ protected function emitArray($result, $array) { $result->out->write('])'); } else { $result->out->write('['); - foreach ($array as $pair) { + foreach ($array->values as $pair) { if ($pair[0]) { $this->emit($result, $pair[0]); $result->out->write('=>'); @@ -518,7 +491,7 @@ protected function emitMethod($result, $method) { protected function emitBraced($result, $braced) { $result->out->write('('); - $this->emit($result, $braced); + $this->emit($result, $braced->expression); $result->out->write(')'); } @@ -554,11 +527,11 @@ protected function emitOffset($result, $offset) { protected function emitAssign($result, $target) { if ('variable' === $target->kind) { - $result->out->write('$'.$target->value); - $result->locals[$target->value]= true; + $result->out->write('$'.$target->name); + $result->locals[$target->name]= true; } else if ('array' === $target->kind) { $result->out->write('list('); - foreach ($target->value as $pair) { + foreach ($target->values as $pair) { $this->emitAssign($result, $pair[1]); $result->out->write(','); } @@ -576,7 +549,7 @@ protected function emitAssignment($result, $assignment) { protected function emitReturn($result, $return) { $result->out->write('return '); - $return && $this->emit($result, $return); + $return->expression && $this->emit($result, $return->expression); $result->out->write(';'); } @@ -639,15 +612,15 @@ protected function emitTry($result, $try) { protected function emitThrow($result, $throw) { $result->out->write('throw '); - $this->emit($result, $throw); + $this->emit($result, $throw->expression); $result->out->write(';'); } protected function emitThrowExpression($result, $throw) { $capture= []; - foreach ($this->search($throw, 'variable') as $var) { - if (isset($result->locals[$var->value])) { - $capture[$var->value]= true; + foreach ($this->search($throw->expression, 'variable') as $var) { + if (isset($result->locals[$var->name])) { + $capture[$var->name]= true; } } unset($capture['this']); @@ -655,7 +628,7 @@ protected function emitThrowExpression($result, $throw) { $result->out->write('(function()'); $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); $result->out->write('{ throw '); - $this->emit($result, $throw); + $this->emit($result, $throw->expression); $result->out->write('; })()'); } @@ -704,28 +677,28 @@ protected function emitWhile($result, $while) { protected function emitBreak($result, $break) { $result->out->write('break '); - $break && $this->emit($result, $break); + $break->expression && $this->emit($result, $break->expression); $result->out->write(';'); } protected function emitContinue($result, $continue) { $result->out->write('continue '); - $continue && $this->emit($result, $continue); + $continue->expression && $this->emit($result, $continue->expression); $result->out->write(';'); } protected function emitLabel($result, $label) { - $result->out->write($label.':'); + $result->out->write($label->name.':'); } protected function emitGoto($result, $goto) { - $result->out->write('goto '.$goto); + $result->out->write('goto '.$goto->label); } protected function emitInstanceOf($result, $instanceof) { $this->emit($result, $instanceof->expression); $result->out->write(' instanceof '); - if ($instanceof->type instanceof Node) { + if ($instanceof->type instanceof Value) { $this->emit($result, $instanceof->type); } else { $result->out->write($instanceof->type); @@ -783,8 +756,8 @@ protected function emitInstance($result, $instance) { $result->out->write('->'); } - if ('name' === $instance->member->kind) { - $result->out->write($instance->member->value); + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); } else { $result->out->write('{'); $this->emit($result, $instance->member); @@ -794,7 +767,7 @@ protected function emitInstance($result, $instance) { protected function emitUnpack($result, $unpack) { $result->out->write('...'); - $this->emit($result, $unpack); + $this->emit($result, $unpack->expression); } protected function emitYield($result, $yield) { @@ -810,7 +783,7 @@ protected function emitYield($result, $yield) { protected function emitFrom($result, $from) { $result->out->write('yield from '); - $this->emit($result, $from); + $this->emit($result, $from->iterable); } public function emit($result, $arg) { @@ -823,7 +796,7 @@ public function emit($result, $arg) { if (isset($this->emit[$arg->kind])) { $this->emit[$arg->kind]($result, $arg); } else { - $this->{'emit'.$arg->kind}($result, $arg->value); + $this->{'emit'.$arg->kind}($result, $arg); } } else { foreach ($arg as $node) { diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 7f051130..5dc70abe 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -2,6 +2,7 @@ use lang\ast\nodes\Assignment; use lang\ast\nodes\BinaryExpression; +use lang\ast\nodes\Literal; use lang\ast\nodes\UnaryExpression; use lang\reflect\Package; @@ -28,9 +29,7 @@ public function symbol($id, $lbp= 0) { public function constant($id, $value) { $const= $this->symbol($id); $const->nud= function($parse, $node) use($value) { - $node->kind= 'literal'; - $node->value= $value; - return $node; + return new Literal($value, $node->line); }; return $const; } @@ -38,9 +37,7 @@ public function constant($id, $value) { public function assignment($id) { $infix= $this->symbol($id, 10); $infix->led= function($parse, $node, $left) use($id) { - $node->kind= 'assignment'; - $node->value= new Assignment($left, $id, $this->expression($parse, 9)); - return $node; + return new Assignment($left, $id, $this->expression($parse, 9), $node->line); }; return $infix; } @@ -48,9 +45,7 @@ public function assignment($id) { public function infix($id, $bp, $led= null) { $infix= $this->symbol($id, $bp); $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($parse, $bp)); - $node->kind= 'binary'; - return $node; + return new BinaryExpression($left, $id, $this->expression($parse, $bp), $node->line); }; return $infix; } @@ -58,9 +53,7 @@ public function infix($id, $bp, $led= null) { public function infixr($id, $bp, $led= null) { $infix= $this->symbol($id, $bp); $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($parse, $bp - 1)); - $node->kind= 'binary'; - return $node; + return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $node->line); }; return $infix; } @@ -68,9 +61,7 @@ public function infixr($id, $bp, $led= null) { public function infixt($id, $bp) { $infix= $this->symbol($id, $bp); $infix->led= function($parse, $node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1)); - $node->kind= 'binary'; - return $node; + return new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1), $node->line); }; return $infix; } @@ -78,9 +69,7 @@ public function infixt($id, $bp) { public function prefix($id, $nud= null) { $prefix= $this->symbol($id); $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $node) use($id) { - $node->value= new UnaryExpression($this->expression($parse, 0), $id); - $node->kind= 'unary'; - return $node; + return new UnaryExpression($this->expression($parse, 0), $id, $node->line); }; return $prefix; } @@ -88,9 +77,7 @@ public function prefix($id, $nud= null) { public function suffix($id, $bp, $led= null) { $suffix= $this->symbol($id, $bp); $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id) { - $node->value= new UnaryExpression($left, $id); - $node->kind= 'unary'; - return $node; + return new UnaryExpression($left, $id, $node->line); }; return $suffix; } diff --git a/src/main/php/lang/ast/language/PHP.class.php b/src/main/php/lang/ast/language/PHP.class.php index 3c6c173a..62387e0f 100755 --- a/src/main/php/lang/ast/language/PHP.class.php +++ b/src/main/php/lang/ast/language/PHP.class.php @@ -7,39 +7,59 @@ use lang\ast\Node; use lang\ast\Type; use lang\ast\UnionType; +use lang\ast\nodes\Annotations; +use lang\ast\nodes\ArrayLiteral; +use lang\ast\nodes\Block; +use lang\ast\nodes\Braced; +use lang\ast\nodes\BreakStatement; use lang\ast\nodes\CaseLabel; use lang\ast\nodes\CastExpression; use lang\ast\nodes\CatchStatement; use lang\ast\nodes\ClassDeclaration; use lang\ast\nodes\ClosureExpression; use lang\ast\nodes\Constant; +use lang\ast\nodes\ContinueStatement; use lang\ast\nodes\DoLoop; +use lang\ast\nodes\EchoStatement; use lang\ast\nodes\ForLoop; use lang\ast\nodes\ForeachLoop; use lang\ast\nodes\FunctionDeclaration; +use lang\ast\nodes\GotoStatement; use lang\ast\nodes\IfStatement; use lang\ast\nodes\InstanceExpression; use lang\ast\nodes\InstanceOfExpression; use lang\ast\nodes\InterfaceDeclaration; use lang\ast\nodes\InvokeExpression; +use lang\ast\nodes\Label; use lang\ast\nodes\LambdaExpression; +use lang\ast\nodes\Literal; use lang\ast\nodes\Method; +use lang\ast\nodes\NamespaceDeclaration; use lang\ast\nodes\NewClassExpression; use lang\ast\nodes\NewExpression; use lang\ast\nodes\NullSafeInstanceExpression; use lang\ast\nodes\OffsetExpression; use lang\ast\nodes\Parameter; use lang\ast\nodes\Property; +use lang\ast\nodes\ReturnStatement; use lang\ast\nodes\ScopeExpression; use lang\ast\nodes\Signature; +use lang\ast\nodes\Start; +use lang\ast\nodes\StaticLocals; use lang\ast\nodes\SwitchStatement; use lang\ast\nodes\TernaryExpression; +use lang\ast\nodes\ThrowExpression; +use lang\ast\nodes\ThrowStatement; use lang\ast\nodes\TraitDeclaration; use lang\ast\nodes\TryStatement; +use lang\ast\nodes\UnpackExpression; use lang\ast\nodes\UseExpression; +use lang\ast\nodes\UseStatement; use lang\ast\nodes\UsingStatement; +use lang\ast\nodes\Variable; use lang\ast\nodes\WhileLoop; use lang\ast\nodes\YieldExpression; +use lang\ast\nodes\YieldFromExpression; /** * PHP language @@ -99,14 +119,12 @@ public function __construct() { $this->infix('instanceof', 60, function($parse, $node, $left) { if ('name' === $parse->token->kind) { - $node->value= new InstanceOfExpression($left, $parse->scope->resolve($parse->token->value)); + $type= $parse->scope->resolve($parse->token->value); $parse->forward(); + return new InstanceOfExpression($left, $type, $node->line); } else { - $node->value= new InstanceOfExpression($left, $this->expression($parse, 0)); + return new InstanceOfExpression($left, $this->expression($parse, 0), $node->line); } - - $node->kind= 'instanceof'; - return $node; }); $this->infix('->', 80, function($parse, $node, $left) { @@ -115,26 +133,33 @@ public function __construct() { $expr= $this->expression($parse, 0); $parse->expecting('}', 'dynamic member'); } else { - $expr= $parse->token; + $expr= new Literal($parse->token->value, $node->line); $parse->forward(); } - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'instance'; - return $node; + return new InstanceExpression($left, $expr, $node->line); }); $this->infix('::', 80, function($parse, $node, $left) { - $node->value= new ScopeExpression($parse->scope->resolve($left->value), $parse->token); - $node->kind= 'scope'; + $scope= $parse->scope->resolve($left->expression); + + if ('variable' === $parse->token->kind) { + $expr= new Variable($parse->token->value, $parse->token->line); + } else if ('name' === $parse->token->kind) { + $expr= new Literal($parse->token->value, $parse->token->line); + } else { + $parse->expecting('name or variable', '::'); + $expr= null; + } + $parse->forward(); - return $node; + return new ScopeExpression($scope, $expr, $node->line); }); $this->infix('==>', 80, function($parse, $node, $left) { $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - $signature= new Signature([new Parameter($left->value, null)], null); + $signature= new Signature([new Parameter($left->name, null)], null); if ('{' === $parse->token->value) { $parse->forward(); $statements= $this->statements($parse); @@ -143,17 +168,13 @@ public function __construct() { $statements= $this->expressionWithThrows($parse, 0); } - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; + return new LambdaExpression($signature, $statements, $node->line); }); $this->infix('(', 80, function($parse, $node, $left) { $arguments= $this->expressions($parse); $parse->expecting(')', 'invoke expression'); - $node->value= new InvokeExpression($left, $arguments); - $node->kind= 'invoke'; - return $node; + return new InvokeExpression($left, $arguments, $node->line); }); $this->infix('[', 80, function($parse, $node, $left) { @@ -165,27 +186,20 @@ public function __construct() { $parse->expecting(']', 'offset access'); } - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; + return new OffsetExpression($left, $expr, $node->line); }); $this->infix('{', 80, function($parse, $node, $left) { $expr= $this->expression($parse, 0); $parse->expecting('}', 'dynamic member'); - - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; + return new OffsetExpression($left, $expr, $node->line); }); $this->infix('?', 80, function($parse, $node, $left) { $when= $this->expressionWithThrows($parse, 0); $parse->expecting(':', 'ternary'); $else= $this->expressionWithThrows($parse, 0); - $node->value= new TernaryExpression($left, $when, $else); - $node->kind= 'ternary'; - return $node; + return new TernaryExpression($left, $when, $else, $node->line); }); $this->prefix('@'); @@ -246,8 +260,6 @@ public function __construct() { if (':' === $parse->token->value || '==>' === $parse->token->value) { $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - - $node->kind= 'lambda'; $parse->forward(); $signature= $this->signature($parse); $parse->forward(); @@ -260,24 +272,20 @@ public function __construct() { $statements= $this->expressionWithThrows($parse, 0); } - $node->value= new LambdaExpression($signature, $statements); + return new LambdaExpression($signature, $statements, $node->line); } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { - $node->kind= 'cast'; - $parse->forward(); $parse->expecting('(', 'cast'); $type= $this->type0($parse, false); $parse->expecting(')', 'cast'); - $node->value= new CastExpression($type, $this->expression($parse, 0)); + return new CastExpression($type, $this->expression($parse, 0), $node->line); } else { - $node->kind= 'braced'; - $parse->forward(); $parse->expecting('(', 'braced'); - $node->value= $this->expression($parse, 0); + $expr= $this->expression($parse, 0); $parse->expecting(')', 'braced'); + return new Braced($expr, $node->line); } - return $node; }); $this->prefix('[', function($parse, $node) { @@ -300,9 +308,7 @@ public function __construct() { } $parse->expecting(']', 'array literal'); - $node->kind= 'array'; - $node->value= $values; - return $node; + return new ArrayLiteral($values, $node->line); }); $this->prefix('new', function($parse, $node) { @@ -314,48 +320,37 @@ public function __construct() { $parse->expecting(')', 'new arguments'); if ('variable' === $type->kind) { - $node->value= new NewExpression('$'.$type->value, $arguments); - $node->kind= 'new'; + return new NewExpression('$'.$type->value, $arguments, $node->line); } else if ('class' === $type->value) { - $node->value= new NewClassExpression($this->clazz($parse, null), $arguments); - $node->kind= 'newclass'; + return new NewClassExpression($this->clazz($parse, null), $arguments, $node->line); } else { - $node->value= new NewExpression($parse->scope->resolve($type->value), $arguments); - $node->kind= 'new'; + return new NewExpression($parse->scope->resolve($type->value), $arguments, $node->line); } - return $node; }); $this->prefix('yield', function($parse, $node) { if (';' === $parse->token->value) { - $node->kind= 'yield'; - $node->value= new YieldExpression(null, null); + return new YieldExpression(null, null, $node->line); } else if ('from' === $parse->token->value) { $parse->forward(); - $node->kind= 'from'; - $node->value= $this->expression($parse, 0); + return new YieldFromExpression($this->expression($parse, 0), $node->line); } else { - $node->kind= 'yield'; $expr= $this->expression($parse, 0); if ('=>' === $parse->token->value) { $parse->forward(); - $node->value= new YieldExpression($expr, $this->expression($parse, 0)); + return new YieldExpression($expr, $this->expression($parse, 0), $node->line); } else { - $node->value= new YieldExpression(null, $expr); + return new YieldExpression(null, $expr, $node->line); } } - return $node; }); $this->prefix('...', function($parse, $node) { - $node->kind= 'unpack'; - $node->value= $this->expression($parse, 0); - return $node; + return new UnpackExpression($this->expression($parse, 0), $node->line); }); $this->prefix('fn', function($parse, $node) { $signature= $this->signature($parse); - $parse->expecting('=>', 'fn'); if ('{' === $parse->token->value) { @@ -366,9 +361,7 @@ public function __construct() { $statements= $this->expressionWithThrows($parse, 0); } - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; + return new LambdaExpression($signature, $statements, $node->line); }); @@ -377,7 +370,6 @@ public function __construct() { // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; // the latter explicitely becomes a statement by pushing a semicolon. if ('(' === $parse->token->value) { - $node->kind= 'closure'; $signature= $this->signature($parse); if ('use' === $parse->token->value) { @@ -404,20 +396,16 @@ public function __construct() { $statements= $this->statements($parse); $parse->expecting('}', 'function'); - $node->value= new ClosureExpression($signature, $use, $statements); + return new ClosureExpression($signature, $use, $statements, $node->line); } else { - $node->kind= 'function'; $name= $parse->token->value; $parse->forward(); $signature= $this->signature($parse); if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' - $n= new Node($parse->token->symbol); $parse->forward(); - $n->value= $this->expressionWithThrows($parse, 0); - $n->line= $parse->token->line; - $n->kind= 'return'; - $statements= [$n]; + $expr= $this->expressionWithThrows($parse, 0); + $statements= [new ReturnStatement($expr, $node->line)]; $parse->expecting(';', 'function'); } else { // Regular function $parse->expecting('{', 'function'); @@ -427,99 +415,97 @@ public function __construct() { $parse->queue= [$parse->token]; $parse->token= new Node($this->symbol(';')); - $node->value= new FunctionDeclaration($name, $signature, $statements); + return new FunctionDeclaration($name, $signature, $statements, $node->line); } - - return $node; }); $this->prefix('static', function($parse, $node) { if ('variable' === $parse->token->kind) { - $node->kind= 'static'; - $node->value= []; + $init= []; while (';' !== $parse->token->value) { $variable= $parse->token->value; $parse->forward(); if ('=' === $parse->token->value) { $parse->forward(); - $initial= $this->expression($parse, 0); + $init[$variable]= $this->expression($parse, 0); } else { - $initial= null; + $init[$variable]= null; } - $node->value[$variable]= $initial; if (',' === $parse->token->value) { $parse->forward(); } } + return new StaticLocals($init, $node->line); } - return $node; + return new Literal($node->value, $node->line); }); $this->prefix('goto', function($parse, $node) { - $node->kind= 'goto'; - $node->value= $parse->token->value; + $label= $parse->token->value; $parse->forward(); - return $node; + return new GotoStatement($label, $node->line); }); $this->prefix('(name)', function($parse, $node) { if (':' === $parse->token->value) { - $node->kind= 'label'; $parse->token= new Node($this->symbol(';')); + return new Label($node->value, $node->line); + } else { + return new Literal($node->value, $node->line); } - return $node; }); - $this->stmt('kind= 'start'; - $node->value= $parse->token->value; + $this->prefix('(variable)', function($parse, $node) { + return new Variable($node->value, $node->line); + }); + + $this->prefix('(literal)', function($parse, $node) { + return new Literal($node->value, $node->line); + }); + + $this->stmt('token->value; $parse->forward(); - return $node; + return new Start($syntax, $node->line); }); $this->stmt('{', function($parse, $node) { - $node->kind= 'block'; - $node->value= $this->statements($parse); - $parse->forward(); - return $node; + $statements= $this->statements($parse); + $parse->expecting('}', 'block'); + return new Block($statements, $node->line); }); $this->prefix('echo', function($parse, $node) { - $node->kind= 'echo'; - $node->value= $this->expressions($parse, ';'); - return $node; + return new EchoStatement($this->expressions($parse, ';'), $node->line); }); $this->stmt('namespace', function($parse, $node) { - $node->kind= 'package'; - $node->value= $parse->token->value; - + $name= $parse->token->value; $parse->forward(); $parse->expecting(';', 'namespace'); - - $parse->scope->package($node->value); - return $node; + $parse->scope->package($name); + return new NamespaceDeclaration($name, $node->line); }); $this->stmt('use', function($parse, $node) { if ('function' === $parse->token->value) { - $node->kind= 'importfunction'; + $type= 'function'; $parse->forward(); } else if ('const' === $parse->token->value) { - $node->kind= 'importconst'; + $type= 'const'; $parse->forward(); } else { - $node->kind= 'import'; + $type= null; // class, interface or trait } $import= $parse->token->value; $parse->forward(); if ('{' === $parse->token->value) { - $types= []; + $names= []; $parse->forward(); while ('}' !== $parse->token->value) { $class= $import.$parse->token->value; @@ -527,11 +513,11 @@ public function __construct() { $parse->forward(); if ('as' === $parse->token->value) { $parse->forward(); - $types[$class]= $parse->token->value; + $names[$class]= $parse->token->value; $parse->scope->import($parse->token->value); $parse->forward(); } else { - $types[$class]= null; + $names[$class]= null; $parse->scope->import($class); } @@ -547,26 +533,23 @@ public function __construct() { $parse->forward(); } else if ('as' === $parse->token->value) { $parse->forward(); - $types= [$import => $parse->token->value]; + $names= [$import => $parse->token->value]; $parse->scope->import($import, $parse->token->value); $parse->forward(); } else { - $types= [$import => null]; + $names= [$import => null]; $parse->scope->import($import); } $parse->expecting(';', 'use'); - $node->value= $types; - return $node; + return new UseStatement($type, $names, $node->line); }); $this->stmt('if', function($parse, $node) { $parse->expecting('(', 'if'); $condition= $this->expression($parse, 0); $parse->expecting(')', 'if'); - $when= $this->block($parse); - if ('else' === $parse->token->value) { $parse->forward(); $otherwise= $this->block($parse); @@ -574,9 +557,7 @@ public function __construct() { $otherwise= null; } - $node->value= new IfStatement($condition, $when, $otherwise); - $node->kind= 'if'; - return $node; + return new IfStatement($condition, $when, $otherwise, $node->line); }); $this->stmt('switch', function($parse, $node) { @@ -590,61 +571,53 @@ public function __construct() { if ('default' === $parse->token->value) { $parse->forward(); $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel(null, []); + $cases[]= new CaseLabel(null, [], $parse->token->line); } else if ('case' === $parse->token->value) { $parse->forward(); $expr= $this->expression($parse, 0); $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel($expr, []); + $cases[]= new CaseLabel($expr, [], $parse->token->line); } else { $cases[sizeof($cases) - 1]->body[]= $this->statement($parse); } } $parse->forward(); - $node->value= new SwitchStatement($condition, $cases); - $node->kind= 'switch'; - return $node; + return new SwitchStatement($condition, $cases, $node->line); }); $this->stmt('break', function($parse, $node) { if (';' === $parse->token->value) { - $node->value= null; + $expr= null; $parse->forward(); } else { - $node->value= $this->expression($parse, 0); + $expr= $this->expression($parse, 0); $parse->expecting(';', 'break'); } - $node->kind= 'break'; - return $node; + return new BreakStatement($expr, $node->line); }); $this->stmt('continue', function($parse, $node) { if (';' === $parse->token->value) { - $node->value= null; + $expr= null; $parse->forward(); } else { - $node->value= $this->expression($parse, 0); + $expr= $this->expression($parse, 0); $parse->expecting(';', 'continue'); } - $node->kind= 'continue'; - return $node; + return new ContinueStatement($expr, $node->line); }); $this->stmt('do', function($parse, $node) { $block= $this->block($parse); - - $parse->expecting('while', 'while'); - $parse->expecting('(', 'while'); + $parse->expecting('while', 'do'); + $parse->expecting('(', 'do'); $expression= $this->expression($parse, 0); - $parse->expecting(')', 'while'); - $parse->expecting(';', 'while'); - - $node->value= new DoLoop($expression, $block); - $node->kind= 'do'; - return $node; + $parse->expecting(')', 'do'); + $parse->expecting(';', 'do'); + return new DoLoop($expression, $block, $node->line); }); $this->stmt('while', function($parse, $node) { @@ -652,10 +625,7 @@ public function __construct() { $expression= $this->expression($parse, 0); $parse->expecting(')', 'while'); $block= $this->block($parse); - - $node->value= new WhileLoop($expression, $block); - $node->kind= 'while'; - return $node; + return new WhileLoop($expression, $block, $node->line); }); $this->stmt('for', function($parse, $node) { @@ -666,18 +636,13 @@ public function __construct() { $parse->expecting(';', 'for'); $loop= $this->expressions($parse, ')'); $parse->expecting(')', 'for'); - $block= $this->block($parse); - - $node->value= new ForLoop($init, $cond, $loop, $block); - $node->kind= 'for'; - return $node; + return new ForLoop($init, $cond, $loop, $block, $node->line); }); $this->stmt('foreach', function($parse, $node) { $parse->expecting('(', 'foreach'); $expression= $this->expression($parse, 0); - $parse->expecting('as', 'foreach'); $expr= $this->expression($parse, 0); @@ -691,18 +656,14 @@ public function __construct() { } $parse->expecting(')', 'foreach'); - $block= $this->block($parse); - $node->value= new ForeachLoop($expression, $key, $value, $block); - $node->kind= 'foreach'; - return $node; + return new ForeachLoop($expression, $key, $value, $block, $node->line); }); $this->stmt('throw', function($parse, $node) { - $node->value= $this->expression($parse, 0); - $node->kind= 'throw'; + $expr= $this->expression($parse, 0); $parse->expecting(';', 'throw'); - return $node; + return new ThrowStatement($expr, $node->line); }); $this->stmt('try', function($parse, $node) { @@ -728,7 +689,7 @@ public function __construct() { $parse->expecting(')', 'catch'); $parse->expecting('{', 'catch'); - $catches[]= new CatchStatement($types, $variable->value, $this->statements($parse)); + $catches[]= new CatchStatement($types, $variable->value, $this->statements($parse), $parse->token->line); $parse->expecting('}', 'catch'); } @@ -741,9 +702,7 @@ public function __construct() { $finally= null; } - $node->value= new TryStatement($statements, $catches, $finally); - $node->kind= 'try'; - return $node; + return new TryStatement($statements, $catches, $finally, $node->line); }); $this->stmt('return', function($parse, $node) { @@ -755,29 +714,21 @@ public function __construct() { $parse->expecting(';', 'return'); } - $node->value= $expr; - $node->kind= 'return'; - return $node; + return new ReturnStatement($expr, $node->line); }); $this->stmt('abstract', function($parse, $node) { $parse->forward(); $type= $parse->scope->resolve($parse->token->value); $parse->forward(); - - $node->value= $this->clazz($parse, $type, ['abstract']); - $node->kind= 'class'; - return $node; + return $this->clazz($parse, $type, ['abstract']); }); $this->stmt('final', function($parse, $node) { $parse->forward(); $type= $parse->scope->resolve($parse->token->value); $parse->forward(); - - $node->value= $this->clazz($parse, $type, ['final']); - $node->kind= 'class'; - return $node; + return $this->clazz($parse, $type, ['final']); }); $this->stmt('<<', function($parse, $node) { @@ -803,17 +754,14 @@ public function __construct() { } while (null !== $parse->token->value); $parse->forward(); - $node->kind= 'annotation'; - return $node; + return new Annotations([], $node->line); }); $this->stmt('class', function($parse, $node) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); - $node->value= $this->clazz($parse, $type); - $node->kind= 'class'; - return $node; + return $this->clazz($parse, $type); }); $this->stmt('interface', function($parse, $node) { @@ -842,10 +790,9 @@ public function __construct() { $body= $this->typeBody($parse); $parse->expecting('}', 'interface'); - $node->value= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment); - $node->kind= 'interface'; + $decl= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment, $node->line); $parse->scope->annotations= []; - return $node; + return $decl; }); $this->stmt('trait', function($parse, $node) { @@ -858,16 +805,13 @@ public function __construct() { $body= $this->typeBody($parse); $parse->expecting('}', 'trait'); - $node->value= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment); - $node->kind= 'trait'; + $decl= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment, $node->line); $parse->scope->annotations= []; - return $node; + return $decl; }); $this->body('use', function($parse, &$body, $annotations, $modifiers) { - $member= new Node($parse->token->symbol); - $member->kind= 'use'; - $member->line= $parse->token->line; + $line= $parse->token->line; $parse->forward(); $types= []; @@ -904,19 +848,15 @@ public function __construct() { $parse->expecting(';', 'use'); } - $member->value= new UseExpression($types, $aliases); - $body[]= $member; + $body[]= new UseExpression($types, $aliases, $line); }); $this->body('const', function($parse, &$body, $annotations, $modifiers) { - $n= new Node($parse->token->symbol); - $n->kind= 'const'; $parse->forward(); $type= null; while (';' !== $parse->token->value) { - $member= clone $n; - $member->line= $parse->token->line; + $line= $parse->token->line; $first= $parse->token; $parse->forward(); @@ -939,8 +879,7 @@ public function __construct() { } $parse->expecting('=', 'const'); - $member->value= new Constant($modifiers, $name, $type, $this->expression($parse, 0)); - $body[$name]= $member; + $body[$name]= new Constant($modifiers, $name, $type, $this->expression($parse, 0), $line); if (',' === $parse->token->value) { $parse->forward(); } @@ -953,9 +892,7 @@ public function __construct() { }); $this->body('function', function($parse, &$body, $annotations, $modifiers) { - $member= new Node($parse->token->symbol); - $member->kind= 'method'; - $member->line= $parse->token->line; + $line= $parse->token->line; $comment= $parse->comment; $parse->comment= null; @@ -978,20 +915,16 @@ public function __construct() { $parse->expecting(';', 'method declaration'); } else if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); - - $n= new Node($parse->token->symbol); - $n->line= $parse->token->line; $parse->forward(); - $n->value= $this->expressionWithThrows($parse, 0); - $n->kind= 'return'; - $statements= [$n]; + $line= $parse->token->line; + $expr= $this->expressionWithThrows($parse, 0); + $statements= [new ReturnStatement($expr, $line)]; $parse->expecting(';', 'method declaration'); } else { $parse->expecting('{, ; or ==>', 'method declaration'); } - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; + $body[$lookup]= new Method($modifiers, $name, $signature, $statements, $annotations, $comment, $line); }); } @@ -1071,14 +1004,11 @@ private function type0($parse, $optional) { } private function properties($parse, &$body, $annotations, $modifiers, $type) { - $n= new Node($parse->token->symbol); - $n->kind= 'property'; $comment= $parse->comment; $parse->comment= null; while (';' !== $parse->token->value) { - $member= clone $n; - $member->line= $parse->token->line; + $line= $parse->token->line; // Untyped `$a` vs. typed `int $a` if ('variable' === $parse->token->kind) { @@ -1096,12 +1026,11 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { $parse->forward(); if ('=' === $parse->token->value) { $parse->forward(); - $member->value= new Property($modifiers, $name, $type, $this->expression($parse, 0), $annotations, $comment); + $body[$lookup]= new Property($modifiers, $name, $type, $this->expression($parse, 0), $annotations, $comment, $line); } else { - $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); + $body[$lookup]= new Property($modifiers, $name, $type, null, $annotations, $comment, $line); } - $body[$lookup]= $member; if (',' === $parse->token->value) { $parse->forward(); } @@ -1284,6 +1213,7 @@ public function block($parse) { public function clazz($parse, $name, $modifiers= []) { $comment= $parse->comment; $parse->comment= null; + $line= $parse->token->line; $parent= null; if ('extends' === $parse->token->value) { @@ -1312,18 +1242,16 @@ public function clazz($parse, $name, $modifiers= []) { $body= $this->typeBody($parse); $parse->expecting('}', 'class'); - $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $parse->scope->annotations, $comment); + $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $parse->scope->annotations, $comment, $line); $parse->scope->annotations= []; return $return; } public function expressionWithThrows($parse, $bp) { if ('throw' === $parse->token->value) { - $expr= new Node($parse->token->symbol); - $expr->kind= 'throwexpression'; + $line= $parse->token->line; $parse->forward(); - $expr->value= $this->expression($parse, $bp); - return $expr; + return new ThrowExpression($this->expression($parse, $bp), $line); } else { return $this->expression($parse, $bp); } From 2d99da203b5f65c6192fc40cbbc65ebfe8403ce5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:10:44 +0200 Subject: [PATCH 075/926] Adjust to AST API changes --- .../ast/emit/RewriteLambdaExpressions.class.php | 6 +++--- .../php/lang/ast/syntax/CompactMethods.class.php | 13 ++++--------- src/main/php/lang/ast/syntax/NullSafe.class.php | 14 +++++++------- src/main/php/lang/ast/syntax/Using.class.php | 12 +++++------- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index 613e3ff8..c353295b 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -9,9 +9,9 @@ trait RewriteLambdaExpressions { protected function emitLambda($result, $lambda) { $capture= []; - foreach ($this->search($lambda->body, 'variable') as $var) { - if (isset($result->locals[$var->value])) { - $capture[$var->value]= true; + foreach ($this->search($lambda, 'variable') as $var) { + if (isset($result->locals[$var->name])) { + $capture[$var->name]= true; } } unset($capture['this']); diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/CompactMethods.class.php index 3d6c8f5b..00e967d6 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/CompactMethods.class.php @@ -2,14 +2,13 @@ use lang\ast\Node; use lang\ast\nodes\Method; +use lang\ast\nodes\ReturnStatement; class CompactMethods { public function setup($parser, $emitter) { $parser->body('fn', function($parse, &$body, $annotations, $modifiers) { - $member= new Node($parse->token->symbol); - $member->kind= 'method'; - $member->line= $parse->token->line; + $line= $parse->token->line; $comment= $parse->comment; $parse->comment= null; @@ -24,14 +23,10 @@ public function setup($parser, $emitter) { $signature= $this->signature($parse); $parse->expecting('=>', 'compact function'); - $return= new Node($parse->token->symbol); - $return->line= $parse->token->line; - $return->value= $this->expressionWithThrows($parse, 0); - $return->kind= 'return'; + $return= new ReturnStatement($this->expressionWithThrows($parse, 0), $parse->token->line); $parse->expecting(';', 'compact function'); - $member->value= new Method($modifiers, $name, $signature, [$return], $annotations, $comment); - $body[$lookup]= $member; + $body[$lookup]= new Method($modifiers, $name, $signature, [$return], $annotations, $comment, $line); }); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php index 247bce06..f31bf187 100755 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/NullSafe.class.php @@ -15,21 +15,21 @@ public function setup($language, $emitter) { $parse->forward(); } - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'nullsafeinstance'; - return $node; + $value= new InstanceExpression($left, $expr); + $value->kind= 'nullsafeinstance'; + return $value; }); $emitter->handle('nullsafeinstance', function($result, $instance) { $t= $result->temp(); $result->out->write('null === ('.$t.'= '); - $this->emit($result, $instance->value->expression); + $this->emit($result, $instance->expression); $result->out->write(') ? null : '.$t.'->'); - if ('name' === $instance->value->member->kind) { - $result->out->write($instance->value->member->value); + if ('name' === $instance->member->kind) { + $result->out->write($instance->member->value); } else { $result->out->write('{'); - $this->emit($result, $instance->value->member); + $this->emit($result, $instance->member); $result->out->write('}'); } }); diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index a16fcfd2..0ff4ffb9 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -14,17 +14,15 @@ public function setup($language, $emitter) { $statements= $this->statements($parse); $parse->expecting('}', 'using block'); - $node->value= new UsingStatement($arguments, $statements); - $node->kind= 'using'; - return $node; + return new UsingStatement($arguments, $statements); }); $emitter->handle('using', function($result, $node) { $variables= []; - foreach ($node->value->arguments as $expression) { + foreach ($node->arguments as $expression) { switch ($expression->kind) { - case 'variable': $variables[]= '$'.$expression->value; break; - case 'assignment': $variables[]= '$'.$expression->value->variable->value; break; + case 'variable': $variables[]= '$'.$expression->name; break; + case 'assignment': $variables[]= '$'.$expression->variable->name; break; default: $temp= $result->temp(); $variables[]= $temp; $result->out->write($temp.'='); } $this->emit($result, $expression); @@ -32,7 +30,7 @@ public function setup($language, $emitter) { } $result->out->write('try {'); - $this->emit($result, $node->value->body); + $this->emit($result, $node->body); $result->out->write('} finally {'); foreach ($variables as $variable) { From fc0c42832535830c927a4c3f00740a750f2428ed Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:11:36 +0200 Subject: [PATCH 076/926] Require xp-framework/ast 2.0 (ahead of its release) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bd655a37..54a686f6 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "^1.4", + "xp-framework/ast": "dev-refactor/ast as 2.0.0", "php" : ">=5.6.0" }, "require-dev" : { From 19e04120071af99e92b9efa57ea2979bc3f42b53 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:12:09 +0200 Subject: [PATCH 077/926] Rewrite parser tests completely now that AST creation is easy --- .../ast/unittest/emit/LoopsTest.class.php | 40 +++ .../emit/TransformationsTest.class.php | 6 +- .../ast/unittest/parse/BlocksTest.class.php | 22 +- .../parse/CompactFunctionsTest.class.php | 34 ++- .../unittest/parse/ConditionalTest.class.php | 72 +++--- .../unittest/parse/FunctionsTest.class.php | 176 ++++++------- .../ast/unittest/parse/InvokeTest.class.php | 39 +-- .../ast/unittest/parse/LambdasTest.class.php | 42 ++- .../ast/unittest/parse/LiteralsTest.class.php | 50 ++-- .../ast/unittest/parse/LoopsTest.class.php | 153 ++++++----- .../ast/unittest/parse/MembersTest.class.php | 242 ++++++++++-------- .../unittest/parse/NamespacesTest.class.php | 34 ++- .../ast/unittest/parse/OperatorTest.class.php | 154 ++++++----- .../ast/unittest/parse/ParseTest.class.php | 33 +-- .../unittest/parse/StartTokensTest.class.php | 15 +- .../ast/unittest/parse/TypesTest.class.php | 91 ++++--- .../unittest/parse/VariablesTest.class.php | 35 +-- 17 files changed, 706 insertions(+), 532 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index 59505a88..c0b0f7fd 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -91,4 +91,44 @@ public function run() { $this->assertEquals('1,2,3', $r); } + + #[@test] + public function break_while_loop() { + $r= $this->run('class { + public function run() { + $i= 0; + $r= []; + while ($i++ < 5) { + if (4 === $i) { + break; + } else { + $r[]= $i; + } + } + return $r; + } + }'); + + $this->assertEquals([1, 2, 3], $r); + } + + #[@test] + public function continue_while_loop() { + $r= $this->run('class { + public function run() { + $i= 0; + $r= []; + while ($i++ < 5) { + if (1 === $i) { + continue; + } else { + $r[]= $i; + } + } + return $r; + } + }'); + + $this->assertEquals([2, 3, 4], $r); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index f8192130..3934d304 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -9,9 +9,9 @@ class TransformationsTest extends EmittingTest { #[@beforeClass] public static function registerTransformation() { self::transform('class', function($class) { - if ($class->value->annotation('getters')) { - foreach ($class->value->properties() as $property) { - $class->value->inject(new Method( + if ($class->annotation('getters')) { + foreach ($class->properties() as $property) { + $class->inject(new Method( ['public'], $property->name, new Signature([], $property->type), diff --git a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php index c8b7fde5..45071b21 100755 --- a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php @@ -1,18 +1,24 @@ block= [['(' => [['(name)' => 'block'], []]]]; + #[@test] + public function empty_block() { + $this->assertParsed( + [new Block([], self::LINE)], + '{ }' + ); } #[@test] - public function static_variable() { - $this->assertNodes( - [['{' => $this->block]], - $this->parse('{ block(); }') + public function with_invoke() { + $this->assertParsed( + [new Block([new InvokeExpression(new Literal('block', self::LINE), [], self::LINE)], self::LINE)], + '{ block(); }' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php index 2283f079..e88f3ec8 100755 --- a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php @@ -1,24 +1,36 @@ return= new ReturnStatement(new Literal('null', self::LINE), self::LINE); + } #[@test] - public function function_returning_null() { - $this->assertNodes( - [['function' => ['a', [[], null], [['==>' => ['null' => 'null']]]]]], - $this->parse('function a() ==> null;') + public function compact_function() { + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [$this->return], self::LINE)], + 'function a() ==> null;' ); \xp::gc(); } #[@test] - public function short_method() { - $block= [['==>' => ['true' => 'true']]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], [], $block, null]] - ], [], null]]], - $this->parse('class A { public function a() ==> true; }') + public function compact_method() { + $method= new Method(['public'], 'a', new Signature([], null), [$this->return], [], null, self::LINE); + + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], [$method->lookup() => $method], [], null, self::LINE)], + 'class A { public function a() ==> null; }' ); \xp::gc(); } diff --git a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php index 9310f75a..0ee65dd6 100755 --- a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php @@ -1,84 +1,90 @@ blocks= [ - [['(' => [['(name)' => 'action1'], []]]], - [['(' => [['(name)' => 'action2'], []]]] + 1 => [new InvokeExpression(new Literal('action1', self::LINE), [], self::LINE)], + 2 => [new InvokeExpression(new Literal('action2', self::LINE), [], self::LINE)] ]; } #[@test] public function plain_if() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], null]]], - $this->parse('if ($condition) { action1(); }') + $this->assertParsed( + [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], null, self::LINE)], + 'if ($condition) { action1(); }' ); } #[@test] public function if_with_else() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], $this->blocks[1]]]], - $this->parse('if ($condition) { action1(); } else { action2(); }') + $this->assertParsed( + [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], $this->blocks[2], self::LINE)], + 'if ($condition) { action1(); } else { action2(); }' ); } #[@test] public function shortcut_if() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], null]]], - $this->parse('if ($condition) action1();') + $this->assertParsed( + [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], null, self::LINE)], + 'if ($condition) action1();' ); } #[@test] public function shortcut_if_else() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], $this->blocks[1]]]], - $this->parse('if ($condition) action1(); else action2();') + $this->assertParsed( + [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], $this->blocks[2], self::LINE)], + 'if ($condition) action1(); else action2();' ); } #[@test] public function empty_switch() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], []]]], - $this->parse('switch ($condition) { }') + $this->assertParsed( + [new SwitchStatement(new Variable('condition', self::LINE), [], self::LINE)], + 'switch ($condition) { }' ); } #[@test] public function switch_with_one_case() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [['(literal)' => '1'], $this->blocks[0]], - ]]]], - $this->parse('switch ($condition) { case 1: action1(); }') + $cases= [new CaseLabel(new Literal('1', self::LINE), $this->blocks[1], self::LINE)]; + $this->assertParsed( + [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], + 'switch ($condition) { case 1: action1(); }' ); } #[@test] public function switch_with_two_cases() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [['(literal)' => '1'], $this->blocks[0]], - [['(literal)' => '2'], $this->blocks[1]], - ]]]], - $this->parse('switch ($condition) { case 1: action1(); case 2: action2(); }') + $cases= [ + new CaseLabel(new Literal('1', self::LINE), $this->blocks[1], self::LINE), + new CaseLabel(new Literal('2', self::LINE), $this->blocks[2], self::LINE) + ]; + $this->assertParsed( + [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], + 'switch ($condition) { case 1: action1(); case 2: action2(); }' ); } #[@test] public function switch_with_default() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [null, $this->blocks[0]] - ]]]], - $this->parse('switch ($condition) { default: action1(); }') + $cases= [new CaseLabel(null, $this->blocks[1], self::LINE)]; + $this->assertParsed( + [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], + 'switch ($condition) { default: action1(); }' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php index 25a5d15f..9b7b5dfb 100755 --- a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php @@ -2,191 +2,167 @@ use lang\ast\FunctionType; use lang\ast\Type; +use lang\ast\nodes\ArrayLiteral; +use lang\ast\nodes\BinaryExpression; +use lang\ast\nodes\FunctionDeclaration; +use lang\ast\nodes\Literal; +use lang\ast\nodes\Parameter; +use lang\ast\nodes\ReturnStatement; +use lang\ast\nodes\Signature; +use lang\ast\nodes\YieldExpression; +use lang\ast\nodes\YieldFromExpression; class FunctionsTest extends ParseTest { #[@test] public function empty_function_without_parameters() { - $this->assertNodes( - [['function' => ['a', [[], null], []]]], - $this->parse('function a() { }') + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [], self::LINE)], + 'function a() { }' ); } #[@test] public function two_functions() { - $this->assertNodes( - [['function' => ['a', [[], null], []]], ['function' => ['b', [[], null], []]]], - $this->parse('function a() { } function b() { }') + $this->assertParsed( + [ + new FunctionDeclaration('a', new Signature([], null), [], self::LINE), + new FunctionDeclaration('b', new Signature([], null), [], self::LINE) + ], + 'function a() { } function b() { }' ); } #[@test, @values(['param', 'protected'])] public function with_parameter($name) { - $this->assertNodes( - [['function' => ['a', [[[$name, false, null, false, null, null, []]], null], []]]], - $this->parse('function a($'.$name.') { }') + $params= [new Parameter($name, null, null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a($'.$name.') { }' ); } #[@test] public function with_reference_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', true, null, false, null, null, []]], null], []]]], - $this->parse('function a(&$param) { }') + $params= [new Parameter('param', null, null, true, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a(&$param) { }' ); } #[@test] public function dangling_comma_in_parameter_lists() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, false, null, null, []]], null], []]]], - $this->parse('function a($param, ) { }') + $params= [new Parameter('param', null, null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a($param, ) { }' ); } #[@test] public function with_typed_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, new Type('string'), false, null, null, []]], null], []]]], - $this->parse('function a(string $param) { }') + $params= [new Parameter('param', new Type('string'), null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a(string $param) { }' ); } #[@test] public function with_nullable_typed_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, new Type('?string'), false, null, null, []]], null], []]]], - $this->parse('function a(?string $param) { }') + $params= [new Parameter('param', new Type('?string'), null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a(?string $param) { }' ); } #[@test] public function with_variadic_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, true, null, null, []]], null], []]]], - $this->parse('function a(... $param) { }') + $params= [new Parameter('param', null, null, false, true, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a(... $param) { }' ); } #[@test] public function with_optional_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, false, null, ['null' => 'null'], []]], null], []]]], - $this->parse('function a($param= null) { }') + $params= [new Parameter('param', null, new Literal('null', self::LINE), false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a($param= null) { }' ); } #[@test] public function with_parameter_named_function() { - $this->assertNodes( - [['function' => ['a', [[['function', false, null, false, null, null, []]], null], []]]], - $this->parse('function a($function) { }') + $params= [new Parameter('function', null, null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a($function, ) { }' ); } #[@test] public function with_typed_parameter_named_function() { - $this->assertNodes( - [['function' => ['a', [[['function', false, new FunctionType([], new Type('void')), false, null, null, []]], null], []]]], - $this->parse('function a((function(): void) $function) { }') + $params= [new Parameter('function', new FunctionType([], new Type('void')), null, false, false, null, [])]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], + 'function a((function(): void) $function) { }' ); } #[@test] public function with_return_type() { - $this->assertNodes( - [['function' => ['a', [[], new Type('void')], []]]], - $this->parse('function a(): void { }') + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], new Type('void')), [], self::LINE)], + 'function a(): void { }' ); } #[@test] public function with_nullable_return() { - $this->assertNodes( - [['function' => ['a', [[], new Type('?string')], []]]], - $this->parse('function a(): ?string { }') - ); - } - - #[@test] - public function default_closure() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], null, [['return' => $block]]]]], - $this->parse('function() { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_use_by_value() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], ['$a', '$b'], [['return' => $block]]]]], - $this->parse('function() use($a, $b) { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_use_by_reference() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], ['$a', '&$b'], [['return' => $block]]]]], - $this->parse('function() use($a, &$b) { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_return_type() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], new Type('int')], null, [['return' => $block]]]]], - $this->parse('function(): int { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_nullable_return_type() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], new Type('?int')], null, [['return' => $block]]]]], - $this->parse('function(): ?int { return $a + 1; };') + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], new Type('?string')), [], self::LINE)], + 'function a(): ?string { }' ); } #[@test] public function generator() { - $statement= ['yield' => [null, null]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield; }') + $yield= new YieldExpression(null, null, self::LINE); + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], + 'function a() { yield; }' ); } #[@test] public function generator_with_value() { - $statement= ['yield' => [null, ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield 1; }') + $yield= new YieldExpression(null, new Literal('1', self::LINE), self::LINE); + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], + 'function a() { yield 1; }' ); } #[@test] public function generator_with_key_and_value() { - $statement= ['yield' => [['(literal)' => '"number"'], ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield "number" => 1; }') + $yield= new YieldExpression(new Literal('"number"', self::LINE), new Literal('1', self::LINE), self::LINE); + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], + 'function a() { yield "number" => 1; }' ); } #[@test] public function generator_delegation() { - $statement= ['yield' => ['[' => []]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield from []; }') + $yield= new YieldFromExpression(new ArrayLiteral([], self::LINE), self::LINE); + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], + 'function a() { yield from []; }' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php index a0c2fb44..1ff1c77a 100755 --- a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php @@ -1,5 +1,10 @@ assertNodes( - [['(' => [['(name)' => 'test'], []]]], - $this->parse('test();') + $this->assertParsed( + [new InvokeExpression(new Literal('test', self::LINE), [], self::LINE)], + 'test();' ); } #[@test] public function invoke_method() { - $this->assertNodes( - [['(' => [['->' => [['(variable)' => 'this'], ['(name)' => 'test']]], []]]], - $this->parse('$this->test();') + $instance= new InstanceExpression(new Variable('this', self::LINE), new Literal('test', self::LINE), self::LINE); + $this->assertParsed( + [new InvokeExpression($instance, [], self::LINE)], + '$this->test();' ); } #[@test] public function invoke_function_with_argument() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1']]]]], - $this->parse('test(1);') + $arguments= [new Literal('1', self::LINE)]; + $this->assertParsed( + [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], + 'test(1);' ); } #[@test] public function invoke_function_with_arguments() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1'], ['(literal)' => '2']]]]], - $this->parse('test(1, 2);') + $arguments= [new Literal('1', self::LINE), new Literal('2', self::LINE)]; + $this->assertParsed( + [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], + 'test(1, 2);' ); } #[@test] public function invoke_function_with_dangling_comma() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1'], ['(literal)' => '2']]]]], - $this->parse('test(1, 2, );') + $arguments= [new Literal('1', self::LINE), new Literal('2', self::LINE)]; + $this->assertParsed( + [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], + 'test(1, 2, );' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php index 85cf877b..eacf6ad7 100755 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php @@ -1,35 +1,49 @@ expression= new BinaryExpression(new Variable('a', self::LINE), '+', new Literal('1', self::LINE), self::LINE); + } #[@test] public function short_closure() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [[[['a', false, null, false, null, null, []]], null], $block]]], - $this->parse('($a) ==> $a + 1;') + $this->assertParsed( + [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], + '($a) ==> $a + 1;' ); \xp::gc(); } #[@test] public function short_closure_as_arg() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [['(name)' => 'exec'], [ - ['(' => [[[['a', false, null, false, null, null, []]], null], $block]] - ]]]], - $this->parse('exec(($a) ==> $a + 1);') + $this->assertParsed( + [new InvokeExpression( + new Literal('execute', self::LINE), + [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], + self::LINE + )], + 'execute(($a) ==> $a + 1);' ); \xp::gc(); } #[@test] public function short_closure_with_braces() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [[[['a', false, null, false, null, null, []]], null], [['return' => $block]]]]], - $this->parse('($a) ==> { return $a + 1; };') + $this->assertParsed( + [new LambdaExpression(new Signature([new Parameter('a', null)], null), [new ReturnStatement($this->expression, self::LINE)], self::LINE)], + '($a) ==> { return $a + 1; };' ); \xp::gc(); } diff --git a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php index 95acdb9b..55579cf1 100755 --- a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php @@ -1,86 +1,84 @@ assertNodes([['(literal)' => $input]], $this->parse($input.';')); + $this->assertParsed([new Literal($input, self::LINE)], $input.';'); } #[@test, @values(['0x00', '0x01', '0xFF', '0xff'])] public function hexadecimal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); + $this->assertParsed([new Literal($input, self::LINE)], $input.';'); } #[@test, @values(['00', '01', '010', '0777'])] public function octal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); + $this->assertParsed([new Literal($input, self::LINE)], $input.';'); } #[@test, @values(['1.0', '1.5'])] public function decimal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); + $this->assertParsed([new Literal($input, self::LINE)], $input.';'); } #[@test] public function bool_true() { - $this->assertNodes([['true' => 'true']], $this->parse('true;')); + $this->assertParsed([new Literal('true', self::LINE)], 'true;'); } #[@test] public function bool_false() { - $this->assertNodes([['false' => 'false']], $this->parse('false;')); + $this->assertParsed([new Literal('false', self::LINE)], 'false;'); } #[@test] public function null() { - $this->assertNodes([['null' => 'null']], $this->parse('null;')); + $this->assertParsed([new Literal('null', self::LINE)], 'null;'); } #[@test] public function empty_string() { - $this->assertNodes([['(literal)' => '""']], $this->parse('"";')); + $this->assertParsed([new Literal('""', self::LINE)], '"";'); } #[@test] public function non_empty_string() { - $this->assertNodes([['(literal)' => '"Test"']], $this->parse('"Test";')); + $this->assertParsed([new Literal('"Test"', self::LINE)], '"Test";'); } #[@test] public function empty_array() { - $this->assertNodes([['[' => []]], $this->parse('[];')); + $this->assertParsed([new ArrayLiteral([], self::LINE)], '[];'); } #[@test] public function int_array() { - $this->assertNodes( - [['[' => [[null, ['(literal)' => '1']], [null, ['(literal)' => '2']]]]], - $this->parse('[1, 2];') - ); + $pairs= [ + [null, new Literal('1', self::LINE)], + [null, new Literal('2', self::LINE)] + ]; + $this->assertParsed([new ArrayLiteral($pairs, self::LINE)], '[1, 2];'); } #[@test] public function key_value_map() { - $this->assertNodes( - [['[' => [[['(literal)' => '"key"'], ['(literal)' => '"value"']]]]], - $this->parse('["key" => "value"];') - ); + $pair= [new Literal('"key"', self::LINE), new Literal('"value"', self::LINE)]; + $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '["key" => "value"];'); } #[@test] public function dangling_comma_in_array() { - $this->assertNodes( - [['[' => [[null, ['(literal)' => '1']]]]], - $this->parse('[1, ];') - ); + $pair= [null, new Literal('1', self::LINE)]; + $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '[1, ];'); } #[@test] public function dangling_comma_in_key_value_map() { - $this->assertNodes( - [['[' => [[['(literal)' => '"key"'], ['(literal)' => '"value"']]]]], - $this->parse('["key" => "value", ];') - ); + $pair= [new Literal('"key"', self::LINE), new Literal('"value"', self::LINE)]; + $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '["key" => "value", ];'); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php index bffd3db2..32c70813 100755 --- a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php @@ -1,134 +1,169 @@ block= ['(' => [['(name)' => 'loop'], []]]; + $this->loop= new InvokeExpression(new Literal('loop', self::LINE), [], self::LINE); } #[@test] public function foreach_value() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], + $this->assertParsed( + [new ForeachLoop( + new Variable('iterable', self::LINE), null, - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $value) { loop(); }') + new Variable('value', self::LINE), + [$this->loop], + self::LINE + )], + 'foreach ($iterable as $value) { loop(); }' ); } #[@test] public function foreach_key_value() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], - ['(variable)' => 'key'], - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $key => $value) { loop(); }') + $this->assertParsed( + [new ForeachLoop( + new Variable('iterable', self::LINE), + new Variable('key', self::LINE), + new Variable('value', self::LINE), + [$this->loop], + self::LINE + )], + 'foreach ($iterable as $key => $value) { loop(); }' ); } #[@test] public function foreach_value_without_curly_braces() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], + $this->assertParsed( + [new ForeachLoop( + new Variable('iterable', self::LINE), null, - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $value) loop();') + new Variable('value', self::LINE), + [$this->loop], + self::LINE + )], + 'foreach ($iterable as $value) loop();' ); } #[@test] public function for_loop() { - $this->assertNodes( - [['for' => [ - [['=' => [['(variable)' => 'i'], '=', ['(literal)' => '0']]]], - [['<' => [['(variable)' => 'i'], '<', ['(literal)' => '10']]]], - [['++' => [['(variable)' => 'i'], '++']]], - [$this->block] - ]]], - $this->parse('for ($i= 0; $i < 10; $i++) { loop(); }') + $this->assertParsed( + [new ForLoop( + [new Assignment(new Variable('i', self::LINE), '=', new Literal('0', self::LINE), self::LINE)], + [new BinaryExpression(new Variable('i', self::LINE), '<', new Literal('10', self::LINE), self::LINE)], + [new UnaryExpression(new Variable('i', self::LINE), '++', self::LINE)], + [$this->loop], + self::LINE + )], + 'for ($i= 0; $i < 10; $i++) { loop(); }' ); } #[@test] public function while_loop() { - $this->assertNodes( - [['while' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('while ($continue) { loop(); }') + $this->assertParsed( + [new WhileLoop( + new Variable('continue', self::LINE), + [$this->loop], + self::LINE + )], + 'while ($continue) { loop(); }' ); } #[@test] public function while_loop_without_curly_braces() { - $this->assertNodes( - [['while' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('while ($continue) loop();') + $this->assertParsed( + [new WhileLoop( + new Variable('continue', self::LINE), + [$this->loop], + self::LINE + )], + 'while ($continue) loop();' ); } #[@test] public function do_loop() { - $this->assertNodes( - [['do' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('do { loop(); } while ($continue);') + $this->assertParsed( + [new DoLoop( + new Variable('continue', self::LINE), + [$this->loop], + self::LINE + )], + 'do { loop(); } while ($continue);' ); } #[@test] public function do_loop_without_curly_braces() { - $this->assertNodes( - [['do' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('do loop(); while ($continue);') + $this->assertParsed( + [new DoLoop( + new Variable('continue', self::LINE), + [$this->loop], + self::LINE + )], + 'do loop(); while ($continue);' ); } #[@test] public function break_statement() { - $this->assertNodes( - [['break' => null]], - $this->parse('break;') + $this->assertParsed( + [new BreakStatement(null, self::LINE)], + 'break;' ); } #[@test] public function break_statement_with_level() { - $this->assertNodes( - [['break' => ['(literal)' => '2']]], - $this->parse('break 2;') + $this->assertParsed( + [new BreakStatement(new Literal('2', self::LINE), self::LINE)], + 'break 2;' ); } #[@test] public function continue_statement() { - $this->assertNodes( - [['continue' => null]], - $this->parse('continue;') + $this->assertParsed( + [new ContinueStatement(null, self::LINE)], + 'continue;' ); } #[@test] public function continue_statement_with_level() { - $this->assertNodes( - [['continue' => ['(literal)' => '2']]], - $this->parse('continue 2;') + $this->assertParsed( + [new ContinueStatement(new Literal('2', self::LINE), self::LINE)], + 'continue 2;' ); } #[@test] public function goto_statement() { - $this->assertNodes( - [['(name)' => 'start'], $this->block, ['goto' => 'start']], - $this->parse('start: loop(); goto start;') + $this->assertParsed( + [new Label('start', self::LINE), $this->loop, new GotoStatement('start', self::LINE)], + 'start: loop(); goto start;' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index 3f2b55a6..138d257d 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -1,224 +1,266 @@ assertNodes( - [['class' => ['\\A', [], null, [], ['$a' => ['(variable)' => ['a', ['private'], null, null, [], null]]], [], null]]], - $this->parse('class A { private $a; }') + $body= [ + '$a' => new Property(['private'], 'a', null, null, [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private $a; }' ); } #[@test] public function private_instance_properties() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - '$a' => ['(variable)' => ['a', ['private'], null, null, [], null]], - '$b' => ['(variable)' => ['b', ['private'], null, null, [], null]], - ], [], null]]], - $this->parse('class A { private $a, $b; }') + $body= [ + '$a' => new Property(['private'], 'a', null, null, [], null, self::LINE), + '$b' => new Property(['private'], 'b', null, null, [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private $a, $b; }' ); } #[@test] public function private_instance_method() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['private'], [[], null], [], [], null]] - ], [], null]]], - $this->parse('class A { private function a() { } }') + $body= [ + 'a()' => new Method(['private'], 'a', new Signature([], null), [], [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private function a() { } }' ); } #[@test] public function private_static_method() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['private', 'static'], [[], null], [], [], null]] - ], [], null]]], - $this->parse('class A { private static function a() { } }') + $body= [ + 'a()' => new Method(['private', 'static'], 'a', new Signature([], null), [], [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private static function a() { } }' ); } #[@test] public function class_constant() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], ['T' => ['const' => ['T', [], ['(literal)' => '1'], null]]], [], null]]], - $this->parse('class A { const T = 1; }') + $body= [ + 'T' => new Constant([], 'T', null, new Literal('1', self::LINE), self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { const T = 1; }' ); } #[@test] public function class_constants() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'T' => ['const' => ['T', [], ['(literal)' => '1'], null]], - 'S' => ['const' => ['S', [], ['(literal)' => '2'], null]] - ], [], null]]], - $this->parse('class A { const T = 1, S = 2; }') + $body= [ + 'T' => new Constant([], 'T', null, new Literal('1', self::LINE), self::LINE), + 'S' => new Constant([], 'S', null, new Literal('2', self::LINE), self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { const T = 1, S = 2; }' ); } #[@test] public function private_class_constant() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], ['T' => ['const' => ['T', ['private'], ['(literal)' => '1'], null]]], [], null]]], - $this->parse('class A { private const T = 1; }') + $body= [ + 'T' => new Constant(['private'], 'T', null, new Literal('1', self::LINE), self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private const T = 1; }' ); } #[@test] public function method_with_return_type() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], new Type('void')], [], [], null]] - ], [], null]]], - $this->parse('class A { public function a(): void { } }') + $body= [ + 'a()' => new Method(['public'], 'a', new Signature([], new Type('void')), [], [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { public function a(): void { } }' ); } #[@test] public function method_with_annotation() { $annotations= ['test' => null]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], $annotations, [], null]] - ], [], null]]], - $this->parse('class A { <> public function a() { } }') + $body= [ + 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { <> public function a() { } }' ); } #[@test] public function method_with_annotations() { - $annotations= ['test' => null, 'ignore' => ['(literal)' => '"Not implemented"']]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], $annotations, [], null]] - ], [], null]]], - $this->parse('class A { <> public function a() { } }') + $annotations= ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)]; + $body= [ + 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { <> public function a() { } }' ); } #[@test] public function instance_property_access() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(name)' => 'member']]]], - $this->parse('$a->member;') + $this->assertParsed( + [new InstanceExpression(new Variable('a', self::LINE), new Literal('member', self::LINE), self::LINE)], + '$a->member;' ); } #[@test] public function dynamic_instance_property_access_via_variable() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(variable)' => 'member']]]], - $this->parse('$a->{$member};') + $this->assertParsed( + [new InstanceExpression(new Variable('a', self::LINE), new Variable('member', self::LINE), self::LINE)], + '$a->{$member};' ); } #[@test] public function dynamic_instance_property_access_via_expression() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(' => [ - ['->' => [['(variable)' => 'field'], ['(name)' => 'get']]], - [['(variable)' => 'instance']] - ]]]]], - $this->parse('$a->{$field->get($instance)};') + $member= new InvokeExpression( + new InstanceExpression(new Variable('field', self::LINE), new Literal('get', self::LINE), self::LINE), + [new Variable('instance', self::LINE)], + self::LINE + ); + $this->assertParsed( + [new InstanceExpression(new Variable('a', self::LINE), $member, self::LINE)], + '$a->{$field->get($instance)};' ); } #[@test] public function static_property_access() { - $this->assertNodes( - [['::' => ['\\A', ['(variable)' => 'member']]]], - $this->parse('A::$member;') + $this->assertParsed( + [new ScopeExpression('\\A', new Variable('member', self::LINE), self::LINE)], + 'A::$member;' ); } #[@test, @values(['self', 'parent', 'static'])] public function scope_resolution($scope) { - $this->assertNodes( - [['::' => [$scope, ['class' => 'class']]]], - $this->parse($scope.'::class;') + $this->assertParsed( + [new ScopeExpression($scope, new Literal('class', self::LINE), self::LINE)], + $scope.'::class;' ); } #[@test] public function class_resolution() { - $this->assertNodes( - [['::' => ['\\A', ['class' => 'class']]]], - $this->parse('A::class;') + $this->assertParsed( + [new ScopeExpression('\\A', new Literal('class', self::LINE), self::LINE)], + 'A::class;' ); } #[@test] public function instance_method_invocation() { - $this->assertNodes( - [['(' => [['->' => [['(variable)' => 'a'], ['(name)' => 'member']]], [['(literal)' => '1']]]]], - $this->parse('$a->member(1);') + $this->assertParsed( + [new InvokeExpression( + new InstanceExpression(new Variable('a', self::LINE), new Literal('member', self::LINE), self::LINE), + [new Literal('1', self::LINE)], + self::LINE + )], + '$a->member(1);' ); } #[@test] public function static_method_invocation() { - $this->assertNodes( - [['(' => [['::' => ['\\A', ['(name)' => 'member']]], [['(literal)' => '1']]]]], - $this->parse('A::member(1);') + $this->assertParsed( + [new InvokeExpression( + new ScopeExpression('\\A', new Literal('member', self::LINE), self::LINE), + [new Literal('1', self::LINE)], + self::LINE + )], + 'A::member(1);' ); } #[@test] public function typed_property() { - $decl= ['$a' => ['(variable)' => ['a', ['private'], null, new Type('string'), [], null]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a; }') + $body= [ + '$a' => new Property(['private'], 'a', new Type('string'), null, [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private string $a; }' ); } #[@test] public function typed_property_with_value() { - $decl= ['$a' => ['(variable)' => ['a', ['private'], ['(literal)' => '"test"'], new Type('string'), [], null]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a = "test"; }') + $body= [ + '$a' => new Property(['private'], 'a', new Type('string'), new Literal('"test"', self::LINE), [], null, self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private string $a = "test"; }' ); } #[@test] public function typed_properties() { - $decl= [ - '$a' => ['(variable)' => ['a', ['private'], null, new Type('string'), [], null]], - '$b' => ['(variable)' => ['b', ['private'], null, new Type('string'), [], null]], - '$c' => ['(variable)' => ['c', ['private'], null, new Type('int'), [], null]], + $body= [ + '$a' => new Property(['private'], 'a', new Type('string'), null, [], null, self::LINE), + '$b' => new Property(['private'], 'b', new Type('string'), null, [], null, self::LINE), + '$c' => new Property(['private'], 'c', new Type('int'), null, [], null, self::LINE) ]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a, $b, int $c; }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { private string $a, $b, int $c; }' ); } #[@test] public function typed_constant() { - $decl= ['T' => ['const' => ['T', [], ['(literal)' => '1'], new Type('int')]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { const int T = 1; }') + $body= [ + 'T' => new Constant([], 'T', new Type('int'), new Literal('1', self::LINE), self::LINE) + ]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { const int T = 1; }' ); } #[@test] public function typed_constants() { - $decl= [ - 'T' => ['const' => ['T', [], ['(literal)' => '1'], new Type('int')]], - 'S' => ['const' => ['S', [], ['(literal)' => '2'], new Type('int')]], - 'I' => ['const' => ['I', [], ['(literal)' => '"i"'], new Type('string')]], + $body= [ + 'T' => new Constant([], 'T', new Type('int'), new Literal('1', self::LINE), self::LINE), + 'S' => new Constant([], 'S', new Type('int'), new Literal('2', self::LINE), self::LINE), + 'I' => new Constant([], 'I', new Type('string'), new Literal('"i"', self::LINE), self::LINE) ]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { const int T = 1, S = 2, string I = "i"; }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { const int T = 1, S = 2, string I = "i"; }' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php index c6448322..2793df48 100755 --- a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php @@ -1,37 +1,55 @@ assertNodes([['namespace' => 'test']], $this->parse('namespace test;')); + $this->assertParsed( + [new NamespaceDeclaration('test', self::LINE)], + 'namespace test;' + ); } #[@test] public function compound_namespace() { - $this->assertNodes([['namespace' => 'lang\\ast']], $this->parse('namespace lang\ast;')); + $this->assertParsed( + [new NamespaceDeclaration('lang\\ast', self::LINE)], + 'namespace lang\\ast;' + ); } #[@test] public function use_statement() { - $this->assertNodes([['use' => ['lang\ast\Parse' => null]]], $this->parse('use lang\ast\Parse;')); + $this->assertParsed( + [new UseStatement(null, ['lang\ast\Parse' => null], self::LINE)], + 'use lang\\ast\\Parse;' + ); } #[@test] public function use_with_alias() { - $this->assertNodes([['use' => ['lang\ast\Parse' => 'P']]], $this->parse('use lang\ast\Parse as P;')); + $this->assertParsed( + [new UseStatement(null, ['lang\ast\Parse' => 'P'], self::LINE)], + 'use lang\\ast\\Parse as P;' + ); } #[@test] public function grouped_use_statement() { - $this->assertNodes( - [['use' => ['lang\ast\Parse' => null, 'lang\ast\Emitter' => null]]], - $this->parse('use lang\ast\{Parse, Emitter};') + $this->assertParsed( + [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\Emitter' => null], self::LINE)], + 'use lang\\ast\\{Parse, Emitter};' ); } #[@test] public function grouped_use_with_alias() { - $this->assertNodes([['use' => ['lang\ast\Parse' => 'P']]], $this->parse('use lang\ast\{Parse as P};')); + $this->assertParsed( + [new UseStatement(null, ['lang\\ast\\Parse' => 'P'], self::LINE)], + 'use lang\\ast\\{Parse as P};' + ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index 8e94def3..a09d7221 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -1,5 +1,20 @@ >', '<<' #])] public function binary($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') + $this->assertParsed( + [new BinaryExpression(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], + '$a '.$operator.' $b;' ); } #[@test] public function ternary() { - $this->assertNodes( - [['?' => [['(variable)' => 'a'], ['(literal)' => '1'], ['(literal)' => '2']]]], - $this->parse('$a ? 1 : 2;') + $this->assertParsed( + [new TernaryExpression(new Variable('a', self::LINE), new Literal('1', self::LINE), new Literal('2', self::LINE), self::LINE)], + '$a ? 1 : 2;' ); } @@ -29,25 +44,25 @@ public function ternary() { # '>', '>=', '<=', '<', '<=>' #])] public function comparison($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') + $this->assertParsed( + [new BinaryExpression(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], + '$a '.$operator.' $b;' ); } #[@test, @values(['++', '--'])] public function suffix($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator]]], - $this->parse('$a'.$operator.';') + $this->assertParsed( + [new UnaryExpression(new Variable('a', self::LINE), $operator, self::LINE)], + '$a'.$operator.';' ); } #[@test, @values(['!', '~', '-', '+', '++', '--'])] public function prefix($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator]]], - $this->parse(''.$operator.'$a;') + $this->assertParsed( + [new UnaryExpression(new Variable('a', self::LINE), $operator, self::LINE)], + ''.$operator.'$a;' ); } @@ -58,121 +73,134 @@ public function prefix($operator) { # '>>=', '<<=' #])] public function assignment($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') + $this->assertParsed( + [new Assignment(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], + '$a '.$operator.' $b;' ); } #[@test] public function assignment_to_offset() { - $this->assertNodes( - [['=' => [['[' => [['(variable)' => 'a'], ['(literal)' => '0']]], '=', ['(literal)' => '1']]]], - $this->parse('$a[0]= 1;') + $target= new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE); + $this->assertParsed( + [new Assignment($target, '=', new Variable('b', self::LINE), self::LINE)], + '$a[0]= $b;' ); } #[@test] public function destructuring_assignment() { - $this->assertNodes( - [['=' => [['[' => [[null, ['(variable)' => 'a']], [null, ['(variable)' => 'b']]]], '=', ['(' => [['(name)' => 'result'], []]]]]], - $this->parse('[$a, $b]= result();') + $target= new ArrayLiteral([[null, new Variable('a', self::LINE)], [null, new Variable('b', self::LINE)]], self::LINE); + $this->assertParsed( + [new Assignment($target, '=', new Variable('c', self::LINE), self::LINE)], + '[$a, $b]= $c;' ); } #[@test] public function comparison_to_assignment() { - $this->assertNodes( - [['===' => [['(literal)' => '1'], '===', ['(' => ['=' => [['(variable)' => 'a'], '=', ['(literal)' => '1']]]]]]], - $this->parse('1 === ($a= 1);') + $this->assertParsed( + [new BinaryExpression( + new Literal('1', self::LINE), '===', new Braced( + new Assignment(new Variable('a', self::LINE), '=', new Literal('1', self::LINE), self::LINE), + self::LINE + ), + self::LINE + )], + '1 === ($a= 1);' ); } #[@test] public function append_array() { - $this->assertNodes( - [['=' => [['[' => [['(variable)' => 'a'], null]], '=', ['(literal)' => '1']]]], - $this->parse('$a[]= 1;') + $target= new OffsetExpression(new Variable('a', self::LINE), null, self::LINE); + $this->assertParsed( + [new Assignment($target, '=', new Variable('b', self::LINE), self::LINE)], + '$a[]= $b;' ); } #[@test] public function clone_expression() { - $this->assertNodes( - [['clone' => [['(variable)' => 'a'], 'clone']]], - $this->parse('clone $a;') + $this->assertParsed( + [new UnaryExpression(new Variable('a', self::LINE), 'clone', self::LINE)], + 'clone $a;' ); } #[@test] public function error_suppression() { - $this->assertNodes( - [['@' => [['(variable)' => 'a'], '@']]], - $this->parse('@$a;') + $this->assertParsed( + [new UnaryExpression(new Variable('a', self::LINE), '@', self::LINE)], + '@$a;' ); } #[@test] public function reference() { - $this->assertNodes( - [['&' => [['(variable)' => 'a'], '&']]], - $this->parse('&$a;') + $this->assertParsed( + [new UnaryExpression(new Variable('a', self::LINE), '&', self::LINE)], + '&$a;' ); } #[@test] public function new_type() { - $this->assertNodes( - [['new' => ['\\T', []]]], - $this->parse('new T();') + $this->assertParsed( + [new NewExpression('\\T', [], self::LINE)], + 'new T();' ); } #[@test] public function new_type_with_args() { - $this->assertNodes( - [['new' => ['\\T', [['(variable)' => 'a'], ['(variable)' => 'b']]]]], - $this->parse('new T($a, $b);') + $this->assertParsed( + [new NewExpression('\\T', [new Variable('a', self::LINE), new Variable('b', self::LINE)], self::LINE)], + 'new T($a, $b);' ); } #[@test] public function new_anonymous_extends() { - $this->assertNodes( - [['new' => [[null, [], '\\T', [], [], [], null], []]]], - $this->parse('new class() extends T { };') + $declaration= new ClassDeclaration([], null, '\\T', [], [], [], null, self::LINE); + $this->assertParsed( + [new NewClassExpression($declaration, [], self::LINE)], + 'new class() extends T { };' ); } #[@test] public function new_anonymous_implements() { - $this->assertNodes( - [['new' => [[null, [], null, ['\\A', '\\B'], [], [], null], []]]], - $this->parse('new class() implements A, B { };') + $declaration= new ClassDeclaration([], null, null, ['\\A', '\\B'], [], [], null, self::LINE); + $this->assertParsed( + [new NewClassExpression($declaration, [], self::LINE)], + 'new class() implements A, B { };' ); } #[@test] public function precedence_of_object_operator() { - $this->assertNodes( - [['.' => [ - ['->' => [['(variable)' => 'this'], ['(name)' => 'a']]], + $this->assertParsed( + [new BinaryExpression( + new InstanceExpression(new Variable('this', self::LINE), new Literal('a', self::LINE), self::LINE), '.', - ['(literal)' => '"test"'] - ]]], - $this->parse('$this->a."test";') + new Literal('"test"', self::LINE), + self::LINE + )], + '$this->a."test";' ); } #[@test] public function precedence_of_scope_resolution_operator() { - $this->assertNodes( - [['.' => [ - ['::' => ['self', ['class' => 'class']]], + $this->assertParsed( + [new BinaryExpression( + new ScopeExpression('self', new Literal('class', self::LINE), self::LINE), '.', - ['(literal)' => '"test"'] - ]]], - $this->parse('self::class."test";') + new Literal('"test"', self::LINE), + self::LINE + )], + 'self::class."test";' ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php index 82418e59..a208f8b1 100755 --- a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php @@ -9,32 +9,7 @@ use unittest\TestCase; abstract class ParseTest extends TestCase { - - /** - * Transforms nodes for easy comparison - * - * @param var $arg - * @return var - */ - private function value($arg) { - if ($arg instanceof Node) { - return [$arg->symbol->id => $this->value($arg->value)]; - } else if ($arg instanceof Value) { - $r= []; - foreach ((array)$arg as $key => $value) { - $r[]= $this->value($value); - } - return $r; - } else if (is_array($arg)) { - $r= []; - foreach ($arg as $key => $value) { - $r[$key]= $this->value($value); - } - return $r; - } else { - return $arg; - } - } + const LINE = 1; /** * Parse code, returning nodes on at a time @@ -54,10 +29,10 @@ protected function parse($code) { * @throws unittest.AssertionFailedError * @return void */ - protected function assertNodes($expected, $nodes) { + protected function assertParsed($expected, $code) { $actual= []; - foreach ($nodes as $node) { - $actual[]= $this->value($node); + foreach ($this->parse($code) as $node) { + $actual[]= $node; } $this->assertEquals($expected, $actual); } diff --git a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php index 46ab6bd7..3c85866d 100755 --- a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php @@ -1,20 +1,23 @@ assertNodes( - [[' 'php'], ['namespace' => 'test']], - $this->parse('assertParsed( + [new Start('php', self::LINE), new NamespaceDeclaration('test', self::LINE)], + 'assertNodes( - [[' 'hh'], ['namespace' => 'test']], - $this->parse('assertParsed( + [new Start('hh', self::LINE), new NamespaceDeclaration('test', self::LINE)], + 'assertNodes( - [['class' => ['\\A', [], null, [], [], [], null]]], - $this->parse('class A { }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], [], [], null, self::LINE)], + 'class A { }' ); } #[@test] public function class_with_parent() { - $this->assertNodes( - [['class' => ['\\A', [], '\\B', [], [], [], null]]], - $this->parse('class A extends B { }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', '\\B', [], [], [], null, self::LINE)], + 'class A extends B { }' ); } #[@test] public function class_with_interface() { - $this->assertNodes( - [['class' => ['\\A', [], null, ['\\C'], [], [], null]]], - $this->parse('class A implements C { }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, ['\\C'], [], [], null, self::LINE)], + 'class A implements C { }' ); } #[@test] public function class_with_interfaces() { - $this->assertNodes( - [['class' => ['\\A', [], null, ['\\C', '\\D'], [], [], null]]], - $this->parse('class A implements C, D { }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, ['\\C', '\\D'], [], [], null, self::LINE)], + 'class A implements C, D { }' ); } #[@test] public function abstract_class() { - $this->assertNodes( - [['abstract' => ['\\A', ['abstract'], null, [], [], [], null]]], - $this->parse('abstract class A { }') + $this->assertParsed( + [new ClassDeclaration(['abstract'], '\\A', null, [], [], [], null, self::LINE)], + 'abstract class A { }' ); } #[@test] public function final_class() { - $this->assertNodes( - [['final' => ['\\A', ['final'], null, [], [], [], null]]], - $this->parse('final class A { }') + $this->assertParsed( + [new ClassDeclaration(['final'], '\\A', null, [], [], [], null, self::LINE)], + 'final class A { }' ); } #[@test] public function empty_interface() { - $this->assertNodes( - [['interface' => ['\\A', [], [], [], [], null]]], - $this->parse('interface A { }') + $this->assertParsed( + [new InterfaceDeclaration([], '\\A', [], [], [], null, self::LINE)], + 'interface A { }' ); } #[@test] public function interface_with_parent() { - $this->assertNodes( - [['interface' => ['\\A', [], ['\\B'], [], [], null]]], - $this->parse('interface A extends B { }') + $this->assertParsed( + [new InterfaceDeclaration([], '\\A', ['\\B'], [], [], null, self::LINE)], + 'interface A extends B { }' ); } #[@test] public function interface_with_parents() { - $this->assertNodes( - [['interface' => ['\\A', [], ['\\B', '\\C'], [], [], null]]], - $this->parse('interface A extends B, C { }') + $this->assertParsed( + [new InterfaceDeclaration([], '\\A', ['\\B', '\\C'], [], [], null, self::LINE)], + 'interface A extends B, C { }' ); } #[@test] public function empty_trait() { - $this->assertNodes( - [['trait' => ['\\A', [], [], [], null]]], - $this->parse('trait A { }') + $this->assertParsed( + [new TraitDeclaration([], '\\A', [], [], null, self::LINE)], + 'trait A { }' ); } #[@test] public function class_with_trait() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B'], []]]], [], null]]], - $this->parse('class A { use B; }') + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], [new UseExpression(['\\B'], [], self::LINE)], [], null, self::LINE)], + 'class A { use B; }' ); } #[@test] public function class_with_multiple_traits() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B'], []]], ['use' => [['\\C'], []]]], [], null]]], - $this->parse('class A { use B; use C; }') + $body= [new UseExpression(['\\B'], [], self::LINE), new UseExpression(['\\C'], [], self::LINE)]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { use B; use C; }' ); } #[@test] public function class_with_comma_separated_traits() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B', '\\C'], []]]], [], null]]], - $this->parse('class A { use B, C; }') + $body= [new UseExpression(['\\B', '\\C'], [], self::LINE)]; + $this->assertParsed( + [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], + 'class A { use B, C; }' ); } #[@test] public function class_in_namespace() { - $this->assertNodes( - [['namespace' => 'test'], ['class' => ['\\test\\A', [], null, [], [], [], null]]], - $this->parse('namespace test; class A { }') + $this->assertParsed( + [new NamespaceDeclaration('test', self::LINE), new ClassDeclaration([], '\\test\\A', null, [], [], [], null, self::LINE)], + 'namespace test; class A { }' ); } diff --git a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php index 82f402b7..647a82d6 100755 --- a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php @@ -1,44 +1,49 @@ assertNodes( - [['(variable)' => $name]], - $this->parse('$'.$name.';') + $this->assertParsed( + [new Variable($name, self::LINE)], + '$'.$name.';' ); } #[@test] public function static_variable() { - $this->assertNodes( - [['static' => ['v' => null]]], - $this->parse('static $v;') + $this->assertParsed( + [new StaticLocals(['v' => null], self::LINE)], + 'static $v;' ); } #[@test] public function static_variable_with_initialization() { - $this->assertNodes( - [['static' => ['id' => ['(literal)' => '0']]]], - $this->parse('static $id= 0;') + $this->assertParsed( + [new StaticLocals(['id' => new Literal('0', self::LINE)], self::LINE)], + 'static $id= 0;' ); } #[@test] public function array_offset() { - $this->assertNodes( - [['[' => [['(variable)' => 'a'], ['(literal)' => '0']]]], - $this->parse('$a[0];') + $this->assertParsed( + [new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE)], + '$a[0];' ); } #[@test] public function string_offset() { - $this->assertNodes( - [['{' => [['(variable)' => 'a'], ['(literal)' => '0']]]], - $this->parse('$a{0};') + $this->assertParsed( + [new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE)], + '$a{0};' ); } } \ No newline at end of file From a02e350d2575e6f6491e56b0b193453821afb8e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:12:44 +0200 Subject: [PATCH 078/926] Add Node and Symbol classes, which were removed from xp-framework/ast --- src/main/php/lang/ast/Node.class.php | 43 ++++++++++++++++++++++++++ src/main/php/lang/ast/Symbol.class.php | 21 +++++++++++++ 2 files changed, 64 insertions(+) create mode 100755 src/main/php/lang/ast/Node.class.php create mode 100755 src/main/php/lang/ast/Symbol.class.php diff --git a/src/main/php/lang/ast/Node.class.php b/src/main/php/lang/ast/Node.class.php new file mode 100755 index 00000000..aab3ddb5 --- /dev/null +++ b/src/main/php/lang/ast/Node.class.php @@ -0,0 +1,43 @@ +symbol= $symbol; + $this->kind= $kind; + $this->value= $value; + } + + /** @return string */ + public function hashCode() { + return $this->symbol->hashCode().$this->kind.Objects::hashOf($this->value); + } + + /** @return string */ + public function toString() { + return nameof($this).'(kind= '.$this->kind.', value= '.Objects::stringOf($this->value).')'; + } + + /** + * Compares this node to another given value + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value === $this ? 0 : 1; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Symbol.class.php b/src/main/php/lang/ast/Symbol.class.php new file mode 100755 index 00000000..ad997130 --- /dev/null +++ b/src/main/php/lang/ast/Symbol.class.php @@ -0,0 +1,21 @@ +id); + } + + public function hashCode() { + return $this->id; + } + + public function toString() { + return nameof($this).'("'.$this->id.'", lbp= '.$this->lbp.')'; + } + + public function compareTo($that) { + return $that instanceof self ? strcmp($this->id, $that->id) : 1; + } +} \ No newline at end of file From d1c724ae520f4fba5add56054835175a9e31b885 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:13:09 +0200 Subject: [PATCH 079/926] Add more tests for parser and emitter --- .../ast/unittest/emit/BlockTest.class.php | 31 ++++++++ .../lang/ast/unittest/emit/GotoTest.class.php | 33 ++++++++ .../ast/unittest/parse/ClosuresTest.class.php | 77 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/BlockTest.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/GotoTest.class.php create mode 100755 src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php new file mode 100755 index 00000000..5d12740c --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php @@ -0,0 +1,31 @@ +run('class { + public function run() { + { + } + return true; + } + }'); + + $this->assertTrue($r); + } + + #[@test] + public function block_with_assignment() { + $r= $this->run('class { + public function run() { + { + $result= true; + } + return $result; + } + }'); + + $this->assertTrue($r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php new file mode 100755 index 00000000..ff57865e --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php @@ -0,0 +1,33 @@ +run('class { + public function run() { + goto skip; + return false; + skip: return true; + } + }'); + + $this->assertTrue($r); + } + + #[@test] + public function skip_backward() { + $r= $this->run('class { + public function run() { + $return= false; + retry: if ($return) return true; + + $return= true; + goto retry; + return false; + } + }'); + + $this->assertTrue($r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php new file mode 100755 index 00000000..65a90d2b --- /dev/null +++ b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php @@ -0,0 +1,77 @@ +returns= new ReturnStatement( + new BinaryExpression( + new Variable('a', self::LINE), + '+', + new Literal('1', self::LINE), + self::LINE + ), + self::LINE + ); + } + + #[@test] + public function with_body() { + $this->assertParsed( + [new ClosureExpression(new Signature([], null), null, [$this->returns], self::LINE)], + 'function() { return $a + 1; };' + ); + } + + #[@test] + public function with_param() { + $params= [new Parameter('a', null, null, false, false, null, [])]; + $this->assertParsed( + [new ClosureExpression(new Signature($params, null), null, [$this->returns], self::LINE)], + 'function($a) { return $a + 1; };' + ); + } + + #[@test] + public function with_use_by_value() { + $this->assertParsed( + [new ClosureExpression(new Signature([], null), ['$a', '$b'], [$this->returns], self::LINE)], + 'function() use($a, $b) { return $a + 1; };' + ); + } + + #[@test] + public function with_use_by_reference() { + $this->assertParsed( + [new ClosureExpression(new Signature([], null), ['$a', '&$b'], [$this->returns], self::LINE)], + 'function() use($a, &$b) { return $a + 1; };' + ); + } + + #[@test] + public function with_return_type() { + $this->assertParsed( + [new ClosureExpression(new Signature([], new Type('int')), null, [$this->returns], self::LINE)], + 'function(): int { return $a + 1; };' + ); + } + + #[@test] + public function with_nullable_return_type() { + $this->assertParsed( + [new ClosureExpression(new Signature([], new Type('?int')), null, [$this->returns], self::LINE)], + 'function(): ?int { return $a + 1; };' + ); + } +} \ No newline at end of file From 1157e04083dc203eedd761734d358f54649d293a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:27:05 +0200 Subject: [PATCH 080/926] Fix emitLiteral(), emitInvoke() and emitFrom() --- src/main/php/lang/ast/emit/PHP56.class.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index eb4a81f6..99828893 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -91,14 +91,14 @@ class PHP56 extends Emitter { protected function emitLiteral($result, $literal) { - if ('"' === $literal{0}) { + if ('"' === $literal->expression[0]) { $result->out->write(preg_replace_callback( '/\\\\u\{([0-9a-f]+)\}/i', function($matches) { return html_entity_decode('&#'.hexdec($matches[1]).';', ENT_HTML5, \xp::ENCODING); }, - $literal + $literal->expression )); } else { - $result->out->write($literal); + $result->out->write($literal->expression); } } @@ -160,17 +160,17 @@ protected function emitInvoke($result, $invoke) { if ('braced' === $expr->kind) { $t= $result->temp(); $result->out->write('(('.$t.'='); - $this->emit($result, $expr->value); + $this->emit($result, $expr->expression); $result->out->write(') ? '.$t); $result->out->write('('); $this->emitArguments($result, $invoke->arguments); $result->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); } else if ( 'scope' === $expr->kind && - 'name' === $expr->value->member->kind && - isset(self::$keywords[strtolower($expr->value->member->value)]) + 'literal' === $expr->member->kind && + isset(self::$keywords[strtolower($expr->member->expression)]) ) { - $result->out->write($expr->value->type.'::{\''.$expr->value->member->value.'\'}'); + $result->out->write($expr->type.'::{\''.$expr->member->expression.'\'}'); $result->out->write('('); $this->emitArguments($result, $invoke->arguments); $result->out->write(')'); @@ -217,7 +217,7 @@ protected function emitNewClass($result, $new) { protected function emitFrom($result, $from) { $result->out->write('foreach ('); - $this->emit($result, $from); + $this->emit($result, $from->iterable); $result->out->write(' as $key => $val) yield $key => $val;'); } From 8518ad931351c12633c7a7f6a4a86eafd3a81a98 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Sep 2019 13:34:35 +0200 Subject: [PATCH 081/926] Fix emitThrowExpression() --- src/main/php/lang/ast/emit/PHP56.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 99828893..d80118cb 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -183,8 +183,8 @@ protected function emitInvoke($result, $invoke) { protected function emitThrowExpression($result, $throw) { $capture= []; foreach ($this->search($throw, 'variable') as $var) { - if (isset($result->locals[$var->value])) { - $capture[$var->value]= true; + if (isset($result->locals[$var->name])) { + $capture[$var->name]= true; } } unset($capture['this']); @@ -193,7 +193,7 @@ protected function emitThrowExpression($result, $throw) { $result->out->write('(('.$t.'=function()'); $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); $result->out->write('{ throw '); - $this->emit($result, $throw); + $this->emit($result, $throw->expression); $result->out->write('; }) ? '.$t.'() : null)'); } From fd4cfbf65d76479fe50988b01d70377a89876e99 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Sep 2019 18:04:40 +0200 Subject: [PATCH 082/926] Update dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 54a686f6..9c2506bf 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "dev-refactor/ast as 2.0.0", + "xp-framework/ast": "dev-master as 2.0.0", "php" : ">=5.6.0" }, "require-dev" : { From 1039887215f172383f083fb9c8f80db5a90de40e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Sep 2019 18:13:15 +0200 Subject: [PATCH 083/926] QA: API documentation --- src/main/php/lang/ast/language/PHP.class.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/php/lang/ast/language/PHP.class.php b/src/main/php/lang/ast/language/PHP.class.php index 62387e0f..b161c35a 100755 --- a/src/main/php/lang/ast/language/PHP.class.php +++ b/src/main/php/lang/ast/language/PHP.class.php @@ -63,10 +63,13 @@ /** * PHP language + * + * @see https://github.com/php/php-langspec */ class PHP extends Language { private $body= []; + /** Setup language parser */ public function __construct() { $this->symbol(':'); $this->symbol(';'); From 28dafcb7fe80700469371448d795faea3578d500 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Sep 2019 18:38:38 +0200 Subject: [PATCH 084/926] Separate emit() and emitAll() --- src/main/php/lang/ast/Compiled.class.php | 2 +- src/main/php/lang/ast/Emitter.class.php | 58 +++++++++---------- src/main/php/lang/ast/emit/PHP56.class.php | 2 +- .../emit/RewriteLambdaExpressions.class.php | 2 +- .../lang/ast/emit/RewriteMultiCatch.class.php | 2 +- src/main/php/lang/ast/syntax/Using.class.php | 2 +- .../php/xp/compiler/CompileRunner.class.php | 2 +- .../ast/unittest/emit/EmittingTest.class.php | 2 +- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index e3fbb2c4..da7bbc88 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -16,7 +16,7 @@ public static function bytes($version, $source, $file) { private static function parse($version, $in, $out, $file) { try { $parse= new Parse(self::$lang, new Tokens(new StreamTokenizer($in)), $file); - self::$emit[$version]->emit(new Result($out), $parse->execute()); + self::$emit[$version]->emitAll(new Result($out), $parse->execute()); return $out; } finally { $in->close(); diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index b38a83bc..d04e9c09 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -159,7 +159,7 @@ protected function emitEcho($result, $echo) { protected function emitBlock($result, $block) { $result->out->write('{'); - $this->emit($result, $block->statements); + $this->emitAll($result, $block->statements); $result->out->write('}'); } @@ -286,7 +286,7 @@ protected function emitFunction($result, $function) { $this->emitSignature($result, $function->signature); $result->out->write('{'); - $this->emit($result, $function->body); + $this->emitAll($result, $function->body); $result->out->write('}'); $result->locals= array_pop($result->stack); @@ -306,7 +306,7 @@ protected function emitClosure($result, $closure) { } } $result->out->write('{'); - $this->emit($result, $closure->body); + $this->emitAll($result, $closure->body); $result->out->write('}'); $result->locals= array_pop($result->stack); @@ -319,7 +319,7 @@ protected function emitLambda($result, $lambda) { if (is_array($lambda->body)) { $result->out->write('{'); - $this->emit($result, $lambda->body); + $this->emitAll($result, $lambda->body); $result->out->write('}'); } else { $this->emit($result, $lambda->body); @@ -481,7 +481,7 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'.$promote); - $this->emit($result, $method->body); + $this->emitAll($result, $method->body); $result->out->write('}'); } @@ -557,12 +557,12 @@ protected function emitIf($result, $if) { $result->out->write('if ('); $this->emit($result, $if->expression); $result->out->write(') {'); - $this->emit($result, $if->body); + $this->emitAll($result, $if->body); $result->out->write('}'); if ($if->otherwise) { $result->out->write('else {'); - $this->emit($result, $if->otherwise); + $this->emitAll($result, $if->otherwise); $result->out->write('}'); } } @@ -590,13 +590,13 @@ protected function emitCatch($result, $catch) { } else { $result->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); } - $this->emit($result, $catch->body); + $this->emitAll($result, $catch->body); $result->out->write('}'); } protected function emitTry($result, $try) { $result->out->write('try {'); - $this->emit($result, $try->body); + $this->emitAll($result, $try->body); $result->out->write('}'); if (isset($try->catches)) { foreach ($try->catches as $catch) { @@ -605,7 +605,7 @@ protected function emitTry($result, $try) { } if (isset($try->finally)) { $result->out->write('finally {'); - $this->emit($result, $try->finally); + $this->emitAll($result, $try->finally); $result->out->write('}'); } } @@ -642,7 +642,7 @@ protected function emitForeach($result, $foreach) { } $this->emit($result, $foreach->value); $result->out->write(') {'); - $this->emit($result, $foreach->body); + $this->emitAll($result, $foreach->body); $result->out->write('}'); } @@ -654,14 +654,14 @@ protected function emitFor($result, $for) { $result->out->write(';'); $this->emitArguments($result, $for->loop); $result->out->write(') {'); - $this->emit($result, $for->body); + $this->emitAll($result, $for->body); $result->out->write('}'); } protected function emitDo($result, $do) { $result->out->write('do'); $result->out->write('{'); - $this->emit($result, $do->body); + $this->emitAll($result, $do->body); $result->out->write('} while ('); $this->emit($result, $do->expression); $result->out->write(');'); @@ -671,7 +671,7 @@ protected function emitWhile($result, $while) { $result->out->write('while ('); $this->emit($result, $while->expression); $result->out->write(') {'); - $this->emit($result, $while->body); + $this->emitAll($result, $while->body); $result->out->write('}'); } @@ -786,23 +786,23 @@ protected function emitFrom($result, $from) { $this->emit($result, $from->iterable); } - public function emit($result, $arg) { - if ($arg instanceof Element) { - if ($arg->line > $result->line) { - $result->out->write(str_repeat("\n", $arg->line - $result->line)); - $result->line= $arg->line; - } + public function emitAll($result, $nodes) { + foreach ($nodes as $node) { + $this->emit($result, $node); + $result->out->write(';'); + } + } - if (isset($this->emit[$arg->kind])) { - $this->emit[$arg->kind]($result, $arg); - } else { - $this->{'emit'.$arg->kind}($result, $arg); - } + public function emit($result, $node) { + if ($node->line > $result->line) { + $result->out->write(str_repeat("\n", $node->line - $result->line)); + $result->line= $node->line; + } + + if (isset($this->emit[$node->kind])) { + $this->emit[$node->kind]($result, $node); } else { - foreach ($arg as $node) { - $this->emit($result, $node); - isset($node->symbol->std) || $result->out->write(';'); - } + $this->{'emit'.$node->kind}($result, $node); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index d80118cb..86207491 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -114,7 +114,7 @@ protected function emitCatch($result, $catch) { $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); } - $this->emit($result, $catch->body); + $this->emitAll($result, $catch->body); $result->out->write('}'); } diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index c353295b..912cbb1b 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -34,7 +34,7 @@ protected function emitLambda($result, $lambda) { if (is_array($lambda->body)) { $result->out->write('{'); - $this->emit($result, $lambda->body); + $this->emitAll($result, $lambda->body); $result->out->write('}'); } else { $result->out->write('{ return '); diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php index 5ff4f9cc..648acc80 100755 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php @@ -19,7 +19,7 @@ protected function emitCatch($result, $catch) { $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); } - $this->emit($result, $catch->body); + $this->emitAll($result, $catch->body); $result->out->write('}'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/Using.class.php b/src/main/php/lang/ast/syntax/Using.class.php index 0ff4ffb9..9af28332 100755 --- a/src/main/php/lang/ast/syntax/Using.class.php +++ b/src/main/php/lang/ast/syntax/Using.class.php @@ -30,7 +30,7 @@ public function setup($language, $emitter) { } $result->out->write('try {'); - $this->emit($result, $node->body); + $this->emitAll($result, $node->body); $result->out->write('} finally {'); foreach ($variables as $variable) { diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 56d7cdd1..c7d87318 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -84,7 +84,7 @@ public static function main(array $args) { $t->start(); try { $parse= new Parse($lang, new Tokens(new StreamTokenizer($in)), $file); - $emit->emit(new Result($output->target((string)$path)), $parse->execute()); + $emit->emitAll(new Result($output->target((string)$path)), $parse->execute()); $t->stop(); Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 4feeb46e..49b36b01 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -41,7 +41,7 @@ protected function type($code) { $out= new MemoryOutputStream(); $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); - self::$emitter->emit(new Result(new StringWriter($out)), $parse->execute()); + self::$emitter->emitAll(new Result(new StringWriter($out)), $parse->execute()); // var_dump($out->getBytes()); self::$cl->setClassBytes($name, $out->getBytes()); From bf3c3d8c3784ddb86934a608bbbe994eb8b604c0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Sep 2019 18:46:00 +0200 Subject: [PATCH 085/926] Remove unused return values --- src/main/php/lang/ast/Language.class.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 5dc70abe..5cdeec3c 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -31,7 +31,6 @@ public function constant($id, $value) { $const->nud= function($parse, $node) use($value) { return new Literal($value, $node->line); }; - return $const; } public function assignment($id) { @@ -39,7 +38,6 @@ public function assignment($id) { $infix->led= function($parse, $node, $left) use($id) { return new Assignment($left, $id, $this->expression($parse, 9), $node->line); }; - return $infix; } public function infix($id, $bp, $led= null) { @@ -47,7 +45,6 @@ public function infix($id, $bp, $led= null) { $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { return new BinaryExpression($left, $id, $this->expression($parse, $bp), $node->line); }; - return $infix; } public function infixr($id, $bp, $led= null) { @@ -55,7 +52,6 @@ public function infixr($id, $bp, $led= null) { $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $node->line); }; - return $infix; } public function infixt($id, $bp) { @@ -63,7 +59,6 @@ public function infixt($id, $bp) { $infix->led= function($parse, $node, $left) use($id, $bp) { return new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1), $node->line); }; - return $infix; } public function prefix($id, $nud= null) { @@ -71,7 +66,6 @@ public function prefix($id, $nud= null) { $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $node) use($id) { return new UnaryExpression($this->expression($parse, 0), $id, $node->line); }; - return $prefix; } public function suffix($id, $bp, $led= null) { @@ -79,7 +73,6 @@ public function suffix($id, $bp, $led= null) { $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id) { return new UnaryExpression($left, $id, $node->line); }; - return $suffix; } public function stmt($id, $func) { From f2b691bb2a836ea8cfa70ed6ed5398cf36422119 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 4 Sep 2019 17:49:21 +0200 Subject: [PATCH 086/926] Transformation functions can now return a Node or an iterable --- src/main/php/lang/ast/Emitter.class.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index d04e9c09..265c9482 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -35,8 +35,14 @@ public function transform($kind, $function) { unset($this->emit[$kind]); } else { $this->emit[$kind]= function($result, $arg) use($function) { - foreach ($function($arg) as $n) { - $this->{'emit'.$n->kind}($result, $n); + $r= $function($arg); + if ($r instanceof Value) { + $this->{'emit'.$r->kind}($result, $r); + } else { + foreach ($r as $node) { + $this->{'emit'.$node->kind}($result, $node); + $result->out->write(';'); + } } }; } From 56c2bb2416523d168ae423c323eca1eca92f10a1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 18:58:46 +0200 Subject: [PATCH 087/926] Refactor extensions to live in lang.ast.syntax.[LANGUAGE] --- .../lang/ast/CompilingClassloader.class.php | 4 +- src/main/php/lang/ast/Emitter.class.php | 50 ++++++------- src/main/php/lang/ast/Language.class.php | 20 ++++- .../php/lang/ast/syntax/NullSafe.class.php | 37 ---------- .../ast/{language => syntax}/PHP.class.php | 2 +- src/main/php/lang/ast/syntax/Using.class.php | 44 ----------- .../syntax/{ => php}/CompactMethods.class.php | 21 +++++- .../lang/ast/syntax/php/NullSafe.class.php | 55 ++++++++++++++ .../php/lang/ast/syntax/php/Using.class.php | 74 +++++++++++++++++++ src/main/php/module.xp | 5 -- .../php/xp/compiler/CompileRunner.class.php | 4 +- .../ast/unittest/emit/EmittingTest.class.php | 9 ++- .../emit/TransformationsTest.class.php | 21 +++--- 13 files changed, 212 insertions(+), 134 deletions(-) delete mode 100755 src/main/php/lang/ast/syntax/NullSafe.class.php rename src/main/php/lang/ast/{language => syntax}/PHP.class.php (99%) delete mode 100755 src/main/php/lang/ast/syntax/Using.class.php rename src/main/php/lang/ast/syntax/{ => php}/CompactMethods.class.php (66%) create mode 100755 src/main/php/lang/ast/syntax/php/NullSafe.class.php create mode 100755 src/main/php/lang/ast/syntax/php/Using.class.php diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 8decef7e..5beb12e5 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -22,8 +22,8 @@ private function __construct($emit) { $emitter= $emit->newInstance(); $language= Language::named('PHP'); - foreach (self::$syntax as $syntax) { - $syntax->setup($language, $emitter); + foreach ($language->extensions() as $extension) { + $extension->setup($language, $emitter); } Compiled::$emit[$this->version]= $emitter; diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 265c9482..67a5de8a 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -9,7 +9,7 @@ abstract class Emitter { const METHOD = 1; protected $unsupported= []; - private $emit= []; + private $transformations= []; /** * Selects the correct emitter for a given runtime @@ -30,35 +30,22 @@ public static function forRuntime($runtime) { throw new IllegalArgumentException('XP Compiler does not support '.$runtime.' yet'); } - public function transform($kind, $function) { - if (null === $function) { - unset($this->emit[$kind]); - } else { - $this->emit[$kind]= function($result, $arg) use($function) { - $r= $function($arg); - if ($r instanceof Value) { - $this->{'emit'.$r->kind}($result, $r); - } else { - foreach ($r as $node) { - $this->{'emit'.$node->kind}($result, $node); - $result->out->write(';'); - } - } - }; - } - return $this; - } - /** - * Emit nodes of a certain kind using the given emitter function. Overwrites - * any previous handling, including builtin behavior! + * Transforms nodes of a certain kind using the given function, which + * may return either single node, which will be then emitted, or an + * iterable producing nodes, which will then be emitted as statements. + * Pass NULL to remove the transformation again. * * @param string $kind - * @param function(lang.ast.Result, lang.ast.Node): void $function + * @param ?(function(lang.ast.Node): lang.ast.Node|iterable) $function * @return self */ - public function handle($kind, $function) { - $this->emit[$kind]= $function->bindTo($this, self::class); + public function transform($kind, $function) { + if (null === $function) { + unset($this->transformations[$kind]); + } else { + $this->transformations[$kind]= $function; + } return $this; } @@ -805,8 +792,17 @@ public function emit($result, $node) { $result->line= $node->line; } - if (isset($this->emit[$node->kind])) { - $this->emit[$node->kind]($result, $node); + // Check for transformations + if (isset($this->transformations[$node->kind])) { + $r= $this->transformations[$node->kind]($node); + if ($r instanceof Value) { + $this->{'emit'.$r->kind}($result, $r); + } else { + foreach ($r as $n) { + $this->{'emit'.$n->kind}($result, $n); + $result->out->write(';'); + } + } } else { $this->{'emit'.$node->kind}($result, $node); } diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 5cdeec3c..c5356461 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -150,9 +150,27 @@ public function statements($parse) { return $statements; } + /** + * Returns extension classes for this language. By convention, these are loaded + * from a package with the same name as the class (but in lowercase). + * + * @return iterable + */ + public function extensions() { + foreach (Package::forName(strtolower(static::class))->getClasses() as $class) { + yield $class->newInstance(); + } + } + + /** + * Returns a language with the given name + * + * @param string $name + * @return self + */ public static function named($name) { if (!isset(self::$instance[$name])) { - self::$instance[$name]= Package::forName('lang.ast.language')->loadClass($name)->newinstance(); + self::$instance[$name]= Package::forName('lang.ast.syntax')->loadClass($name)->newInstance(); } return self::$instance[$name]; } diff --git a/src/main/php/lang/ast/syntax/NullSafe.class.php b/src/main/php/lang/ast/syntax/NullSafe.class.php deleted file mode 100755 index f31bf187..00000000 --- a/src/main/php/lang/ast/syntax/NullSafe.class.php +++ /dev/null @@ -1,37 +0,0 @@ -infix('?->', 80, function($parse, $node, $left) { - if ('{' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - $parse->expecting('}', 'dynamic member'); - } else { - $expr= $parse->token; - $parse->forward(); - } - - $value= new InstanceExpression($left, $expr); - $value->kind= 'nullsafeinstance'; - return $value; - }); - $emitter->handle('nullsafeinstance', function($result, $instance) { - $t= $result->temp(); - $result->out->write('null === ('.$t.'= '); - $this->emit($result, $instance->expression); - $result->out->write(') ? null : '.$t.'->'); - - if ('name' === $instance->member->kind) { - $result->out->write($instance->member->value); - } else { - $result->out->write('{'); - $this->emit($result, $instance->member); - $result->out->write('}'); - } - }); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/language/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php similarity index 99% rename from src/main/php/lang/ast/language/PHP.class.php rename to src/main/php/lang/ast/syntax/PHP.class.php index b161c35a..238c93d3 100755 --- a/src/main/php/lang/ast/language/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1,4 +1,4 @@ -stmt('using', function($parse, $node) { - $parse->expecting('(', 'using arguments'); - $arguments= $this->expressions($parse, ')'); - $parse->expecting(')', 'using arguments'); - - $parse->expecting('{', 'using block'); - $statements= $this->statements($parse); - $parse->expecting('}', 'using block'); - - return new UsingStatement($arguments, $statements); - }); - - $emitter->handle('using', function($result, $node) { - $variables= []; - foreach ($node->arguments as $expression) { - switch ($expression->kind) { - case 'variable': $variables[]= '$'.$expression->name; break; - case 'assignment': $variables[]= '$'.$expression->variable->name; break; - default: $temp= $result->temp(); $variables[]= $temp; $result->out->write($temp.'='); - } - $this->emit($result, $expression); - $result->out->write(';'); - } - - $result->out->write('try {'); - $this->emitAll($result, $node->body); - - $result->out->write('} finally {'); - foreach ($variables as $variable) { - $result->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); - $result->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); - $result->out->write('unset('.$variable.');'); - } - $result->out->write('}'); - }); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/CompactMethods.class.php b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php similarity index 66% rename from src/main/php/lang/ast/syntax/CompactMethods.class.php rename to src/main/php/lang/ast/syntax/php/CompactMethods.class.php index 00e967d6..1c0e56d9 100755 --- a/src/main/php/lang/ast/syntax/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php @@ -1,9 +1,28 @@ - $this->name; + * } + * + * // Rewritten to + * class Person { + * private string $name; + * public function name(): string { return $this->name; } + * } + * ``` + * @see https://github.com/xp-framework/rfc/issues/241 + * @test xp://lang.ast.unittest.emit.CompactFunctionsTest + */ class CompactMethods { public function setup($parser, $emitter) { diff --git a/src/main/php/lang/ast/syntax/php/NullSafe.class.php b/src/main/php/lang/ast/syntax/php/NullSafe.class.php new file mode 100755 index 00000000..30ed502d --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/NullSafe.class.php @@ -0,0 +1,55 @@ +method(); + * + * // Rewritten to + * $value= null === ($_N0= $instance) ? null : $_N0->method() + * ``` + * + * @see https://github.com/xp-framework/compiler/issues/9 + * @test xp://lang.ast.unittest.emit.NullSafeTest + */ +class NullSafe { + + public function setup($language, $emitter) { + $language->infix('?->', 80, function($parse, $node, $left) { + if ('{' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + $parse->expecting('}', 'dynamic member'); + } else { + $expr= new Literal($parse->token->value); + $parse->forward(); + } + + $value= new InstanceExpression($left, $expr, $node->line); + $value->kind= 'nullsafeinstance'; + return $value; + }); + + $emitter->transform('nullsafeinstance', function($node) { + static $i= 0; + + $temp= new Variable('_N'.($i++)); + $null= new Literal('null'); + return new TernaryExpression( + new BinaryExpression($null, '===', new Braced(new Assignment($temp, '=', $node->expression))), + $null, + new InstanceExpression($temp, $node->member) + ); + }); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/Using.class.php b/src/main/php/lang/ast/syntax/php/Using.class.php new file mode 100755 index 00000000..947c6870 --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/Using.class.php @@ -0,0 +1,74 @@ +open(File::WRITE); + * $f->write(...); + * } + * + * // Rewritten to + * $f= new File(); + * try { + * $f->open(File::WRITE); + * $f->write(...); + * } finally { + * $f->close(); + * } + * ``` + * + * @see https://github.com/xp-framework/compiler/pull/33 + * @test xp://lang.ast.unittest.emit.UsingTest + */ +class Using { + + public function setup($language, $emitter) { + $language->stmt('using', function($parse, $node) { + $parse->expecting('(', 'using arguments'); + $arguments= $this->expressions($parse, ')'); + $parse->expecting(')', 'using arguments'); + + $parse->expecting('{', 'using block'); + $statements= $this->statements($parse); + $parse->expecting('}', 'using block'); + + return new UsingStatement($arguments, $statements); + }); + + $emitter->transform('using', function($node) { + static $i= 0; + + $cleanup= []; + foreach ($node->arguments as $expression) { + switch ($expression->kind) { + case 'variable': $variable= $expression; yield $expression; break; + case 'assignment': $variable= $expression->variable; yield $expression; break; + default: $variable= new Variable('_U'.($i++)); yield new Assignment($variable, '=', $expression); break; + } + + $cleanup[]= new IfStatement(new InstanceOfExpression($variable, '\lang\Closeable'), + [new InvokeExpression(new InstanceExpression($variable, new Literal('close')), [])], + [new IfStatement(new InstanceOfExpression($variable, '\IDisposable'), + [new InvokeExpression(new InstanceExpression($variable, new Literal('__dispose')), [])] + )] + ); + $cleanup[]= new InvokeExpression(new Literal('unset'), [$variable]); + } + + yield new TryStatement($node->body, null, $cleanup); + }); + } +} \ No newline at end of file diff --git a/src/main/php/module.xp b/src/main/php/module.xp index da9384b2..798880e2 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -1,7 +1,6 @@ getClasses() as $class) { - CompilingClassloader::$syntax[]= $class->newInstance(); - } - if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); } diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index c7d87318..eaac9f5c 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -69,8 +69,8 @@ public static function main(array $args) { $lang= Language::named('PHP'); $emit= Emitter::forRuntime($target)->newInstance(); - foreach (CompilingClassloader::$syntax as $syntax) { - $syntax->setup($lang, $emit); + foreach ($lang->extensions() as $extension) { + $extension->setup($lang, $emit); } $input= Input::newInstance($in); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 49b36b01..d6395a24 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -17,16 +17,17 @@ abstract class EmittingTest extends TestCase { private static $cl, $language, $emitter; private static $id= 0; - static function __static() { + #[@beforeClass] + public static function setupCompiler() { self::$cl= DynamicClassLoader::instanceFor(self::class); self::$language= Language::named('PHP'); self::$emitter= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); - foreach (CompilingClassLoader::$syntax as $syntax) { - $syntax->setup(self::$language, self::$emitter); + foreach (self::$language->extensions() as $extension) { + $extension->setup(self::$language, self::$emitter); } } - protected static function transform($type, $function) { + protected function transform($type, $function) { self::$emitter->transform($type, $function); } diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 3934d304..1778f706 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -6,30 +6,30 @@ class TransformationsTest extends EmittingTest { - #[@beforeClass] - public static function registerTransformation() { - self::transform('class', function($class) { + /** @return void */ + public function setUp() { + $this->transform('class', function($class) { if ($class->annotation('getters')) { foreach ($class->properties() as $property) { $class->inject(new Method( ['public'], $property->name, new Signature([], $property->type), - [new Code('return $this->'.$property->name.';')] + [new Code('return $this->'.$property->name)] )); } } - yield $class; + return $class; }); } - #[@afterClass] - public static function removeTransformation() { - self::transform('class', null); + /** @return void */ + public function tearDown() { + $this->transform('class', null); } - #[@test, @values(['id', 'name'])] - public function generates_accessor($name) { + #[@test, @values([['id', 1], ['name', 'Test']])] + public function generates_accessor($name, $expected) { $t= $this->type('<> class { private int $id; private string $name; @@ -40,5 +40,6 @@ public function __construct(int $id, string $name) { } }'); $this->assertTrue($t->hasMethod($name)); + $this->assertEquals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); } } \ No newline at end of file From 7390bfce4964042be28ecbaace93e99f59b094fb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 19:03:54 +0200 Subject: [PATCH 088/926] Package::forName() does not handle backslashes correctly --- src/main/php/lang/ast/Language.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index c5356461..716d9df7 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -157,7 +157,7 @@ public function statements($parse) { * @return iterable */ public function extensions() { - foreach (Package::forName(strtolower(static::class))->getClasses() as $class) { + foreach (Package::forName(strtr(strtolower(static::class), '\\', '.'))->getClasses() as $class) { yield $class->newInstance(); } } From 6485af5b7afacdf175ff9013c310c2d8669a72b5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:28:47 +0200 Subject: [PATCH 089/926] Ensure transformation API works --- src/main/php/lang/ast/Language.class.php | 30 ++++++++++------------ src/main/php/lang/ast/syntax/PHP.class.php | 16 ++++++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 716d9df7..548b1930 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -4,8 +4,12 @@ use lang\ast\nodes\BinaryExpression; use lang\ast\nodes\Literal; use lang\ast\nodes\UnaryExpression; +use lang\ast\syntax\TransformationApi; use lang\reflect\Package; +/** + * Base class for input languages + */ abstract class Language { private static $instance= []; @@ -80,6 +84,13 @@ public function stmt($id, $func) { $stmt->std= $func->bindTo($this, static::class); } + /** + * Returns a single expression + * + * @param lang.ast.Parse $parse + * @param int $rbp + * @return lang.ast.Node + */ public function expression($parse, $rbp) { $t= $parse->token; $parse->forward(); @@ -94,22 +105,6 @@ public function expression($parse, $rbp) { return $left; } - public function expressions($parse, $end= ')') { - $arguments= []; - while ($end !== $parse->token->value) { - $arguments[]= $this->expression($parse, 0); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ($end === $parse->token->value) { - break; - } else { - $parse->expecting($end.' or ,', 'argument list'); - break; - } - } - return $arguments; - } - /** * Returns a single statement * @@ -151,12 +146,13 @@ public function statements($parse) { } /** - * Returns extension classes for this language. By convention, these are loaded + * Returns extensions for this language. By convention, these are loaded * from a package with the same name as the class (but in lowercase). * * @return iterable */ public function extensions() { + yield new TransformationApi(); foreach (Package::forName(strtr(strtolower(static::class), '\\', '.'))->getClasses() as $class) { yield $class->newInstance(); } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 238c93d3..c10af284 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1250,6 +1250,22 @@ public function clazz($parse, $name, $modifiers= []) { return $return; } + public function expressions($parse, $end= ')') { + $arguments= []; + while ($end !== $parse->token->value) { + $arguments[]= $this->expression($parse, 0); + if (',' === $parse->token->value) { + $parse->forward(); + } else if ($end === $parse->token->value) { + break; + } else { + $parse->expecting($end.' or ,', 'argument list'); + break; + } + } + return $arguments; + } + public function expressionWithThrows($parse, $bp) { if ('throw' === $parse->token->value) { $line= $parse->token->line; From 97300de9fba59a234c0886d1ec5ebe8e20a5dca6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:29:51 +0200 Subject: [PATCH 090/926] Remove Element interface from Nodes --- src/main/php/lang/ast/Node.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Node.class.php b/src/main/php/lang/ast/Node.class.php index aab3ddb5..2d9506a9 100755 --- a/src/main/php/lang/ast/Node.class.php +++ b/src/main/php/lang/ast/Node.class.php @@ -3,7 +3,7 @@ use lang\Value; use util\Objects; -class Node implements Element, Value { +class Node implements Value { public $symbol, $value, $kind; public $comment= null; public $line= -1; From f8d29f599950d15176d537b654714ba60f320561 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:33:59 +0200 Subject: [PATCH 091/926] QA: API docs --- src/main/php/lang/ast/Symbol.class.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Symbol.class.php b/src/main/php/lang/ast/Symbol.class.php index ad997130..6d4c79c5 100755 --- a/src/main/php/lang/ast/Symbol.class.php +++ b/src/main/php/lang/ast/Symbol.class.php @@ -1,21 +1,27 @@ id); - } +class Symbol implements Value { + public $id, $lbp= 0, $std= null, $nud, $led; + /** @return string */ public function hashCode() { return $this->id; } + /** @return string */ public function toString() { return nameof($this).'("'.$this->id.'", lbp= '.$this->lbp.')'; } - public function compareTo($that) { - return $that instanceof self ? strcmp($this->id, $that->id) : 1; + /** + * Compare + * + * @param var $value + * @return int + */ + public function compareTo($value) { + return $value instanceof self ? strcmp($this->id, $value->id) : 1; } } \ No newline at end of file From 6c2c2ba411bd8ffbc158191170e2f1132675953b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:34:37 +0200 Subject: [PATCH 092/926] Fix parent::emitParameter() call --- src/main/php/lang/ast/emit/HHVM320.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/HHVM320.class.php b/src/main/php/lang/ast/emit/HHVM320.class.php index d83dea44..3491c3d1 100755 --- a/src/main/php/lang/ast/emit/HHVM320.class.php +++ b/src/main/php/lang/ast/emit/HHVM320.class.php @@ -22,7 +22,7 @@ protected function emitParameter($result, $parameter) { $result->out->write('... $'.$parameter->name); $result->locals[$parameter->name]= true; } else { - parent::emitParameter($result$parameter); + parent::emitParameter($result, $parameter); } } } \ No newline at end of file From efd713607e484dff96ba4e9bfee1850d589091c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:39:56 +0200 Subject: [PATCH 093/926] Fix usage not showing extensions after refactoring --- src/main/php/xp/compiler/Usage.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 83a395d5..6fc5a2bc 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,6 +1,6 @@ extensions() as $extension) { + $t= typeof($extension); $l= $t->getClassLoader(); $hash= $l->hashCode(); if (isset($sorted[$hash])) { From 6ad9e7d62069d032a378eed647d97096757f46bc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:48:52 +0200 Subject: [PATCH 094/926] Extract Hack-style arrow functions --- src/main/php/lang/ast/syntax/PHP.class.php | 16 ---------- .../syntax/php/HackArrowFunctions.class.php | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index c10af284..8251a0c2 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -159,21 +159,6 @@ public function __construct() { return new ScopeExpression($scope, $expr, $node->line); }); - $this->infix('==>', 80, function($parse, $node, $left) { - $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - - $signature= new Signature([new Parameter($left->name, null)], null); - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'arrow function'); - } else { - $statements= $this->expressionWithThrows($parse, 0); - } - - return new LambdaExpression($signature, $statements, $node->line); - }); - $this->infix('(', 80, function($parse, $node, $left) { $arguments= $this->expressions($parse); $parse->expecting(')', 'invoke expression'); @@ -367,7 +352,6 @@ public function __construct() { return new LambdaExpression($signature, $statements, $node->line); }); - $this->prefix('function', function($parse, $node) { // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; diff --git a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php new file mode 100755 index 00000000..b78c1a1a --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php @@ -0,0 +1,32 @@ +infix('==>', 80, function($parse, $node, $left) { + $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); + + $signature= new Signature([new Parameter($left->name, null)], null); + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'arrow function'); + } else { + $statements= $this->expressionWithThrows($parse, 0); + } + + return new LambdaExpression($signature, $statements, $node->line); + }); + } +} \ No newline at end of file From 44d1946fee588f8da53dac49cf279f8ed5054f62 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 22:52:20 +0200 Subject: [PATCH 095/926] Set annotation values --- src/main/php/lang/ast/syntax/PHP.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 8251a0c2..217545c3 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -719,16 +719,17 @@ public function __construct() { }); $this->stmt('<<', function($parse, $node) { + $values= []; do { $name= $parse->token->value; $parse->forward(); if ('(' === $parse->token->value) { $parse->forward(); - $parse->scope->annotations[$name]= $this->expression($parse, 0); + $values[$name]= $parse->scope->annotations[$name]= $this->expression($parse, 0); $parse->expecting(')', 'annotations'); } else { - $parse->scope->annotations[$name]= null; + $values[$name]= $parse->scope->annotations[$name]= null; } if (',' === $parse->token->value) { @@ -741,7 +742,7 @@ public function __construct() { } while (null !== $parse->token->value); $parse->forward(); - return new Annotations([], $node->line); + return new Annotations($values, $node->line); }); $this->stmt('class', function($parse, $node) { From d321dee541ee9cceb900e98aa4e809ed54f72d5b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 5 Sep 2019 23:05:35 +0200 Subject: [PATCH 096/926] QA: Remove unused import --- src/main/php/lang/ast/syntax/PHP.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 217545c3..05b64e27 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -55,7 +55,6 @@ use lang\ast\nodes\UnpackExpression; use lang\ast\nodes\UseExpression; use lang\ast\nodes\UseStatement; -use lang\ast\nodes\UsingStatement; use lang\ast\nodes\Variable; use lang\ast\nodes\WhileLoop; use lang\ast\nodes\YieldExpression; From 76eba604edd35d29caba01ebe5372ee747bb8c3e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 12:35:21 +0200 Subject: [PATCH 097/926] Fix multiple annotations on types --- src/main/php/lang/ast/syntax/PHP.class.php | 2 ++ .../lang/ast/unittest/emit/AnnotationsTest.class.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 05b64e27..7b9efa9f 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -732,11 +732,13 @@ public function __construct() { } if (',' === $parse->token->value) { + $parse->forward(); continue; } else if ('>>' === $parse->token->value) { break; } else { $parse->expecting(', or >>', 'annotation'); + break; } } while (null !== $parse->token->value); diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 936f781c..1c032572 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -77,4 +77,16 @@ public function params() { [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] ); } + + #[@test] + public function multiple_class_annotations() { + $t= $this->type('<> class { }'); + $this->assertEquals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + } + + #[@test] + public function multiple_member_annotations() { + $t= $this->type('class { <> public function fixture() { } }'); + $this->assertEquals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); + } } \ No newline at end of file From 86a0e42608941bf881b18d61550a47f8d53818fc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 12:54:18 +0200 Subject: [PATCH 098/926] Allow multiple transformations per kind --- src/main/php/lang/ast/Emitter.class.php | 60 ++++++++++++++----- .../lang/ast/unittest/EmitterTest.class.php | 42 ++++++++++++- .../ast/unittest/emit/EmittingTest.class.php | 17 +++++- .../emit/TransformationsTest.class.php | 51 +++++++++++++++- 4 files changed, 149 insertions(+), 21 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 67a5de8a..fc1942f6 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -34,19 +34,42 @@ public static function forRuntime($runtime) { * Transforms nodes of a certain kind using the given function, which * may return either single node, which will be then emitted, or an * iterable producing nodes, which will then be emitted as statements. - * Pass NULL to remove the transformation again. + * Returns a handle to remove the transformation again * * @param string $kind - * @param ?(function(lang.ast.Node): lang.ast.Node|iterable) $function - * @return self + * @param (function(lang.ast.Node): lang.ast.Node|iterable) $function + * @return var */ public function transform($kind, $function) { - if (null === $function) { - unset($this->transformations[$kind]); + if (isset($this->transformations[$kind])) { + $i= sizeof($this->transformations[$kind]); + $this->transformations[$kind][]= $function; } else { - $this->transformations[$kind]= $function; + $i= 0; + $this->transformations[$kind]= [$function]; } - return $this; + return ['kind' => $kind, 'id' => $i]; + } + + /** + * Removes a transformation added with transform() + * + * @param var $transformation + * @return void + */ + public function remove($transformation) { + $kind= $transformation['kind']; + array_splice($this->transformations[$kind], $transformation['id'], 1); + if (empty($this->transformations[$kind])) unset($this->transformations[$kind]); + } + + /** + * Returns all transformations + * + * @return [:var[]] + */ + public function transformations() { + return $this->transformations; } /** @@ -794,17 +817,22 @@ public function emit($result, $node) { // Check for transformations if (isset($this->transformations[$node->kind])) { - $r= $this->transformations[$node->kind]($node); - if ($r instanceof Value) { - $this->{'emit'.$r->kind}($result, $r); - } else { - foreach ($r as $n) { - $this->{'emit'.$n->kind}($result, $n); - $result->out->write(';'); + foreach ($this->transformations[$node->kind] as $transformation) { + $r= $transformation($node); + if ($r instanceof Value) { + if ($r->kind === $node->kind) continue; + $this->{'emit'.$r->kind}($result, $r); + return; + } else if ($r) { + foreach ($r as $n) { + $this->{'emit'.$n->kind}($result, $n); + $result->out->write(';'); + } + return; } } - } else { - $this->{'emit'.$node->kind}($result, $node); + // Fall through, use default } + $this->{'emit'.$node->kind}($result, $node); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 1574df79..40229b03 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -5,8 +5,48 @@ class EmitterTest extends TestCase { + private function newEmitter() { + return Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + } + #[@test] public function can_create() { - Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + $this->newEmitter(); + } + + #[@test] + public function transformations_initially_empty() { + $this->assertEquals([], $this->newEmitter()->transformations()); + } + + #[@test] + public function transform() { + $function= function($class) { return $class; }; + + $fixture= $this->newEmitter(); + $fixture->transform('class', $function); + $this->assertEquals(['class' => [$function]], $fixture->transformations()); + } + + #[@test] + public function remove() { + $first= function($class) { return $class; }; + $second= function($class) { $class->annotations['author']= 'Test'; return $class; }; + + $fixture= $this->newEmitter(); + $transformation= $fixture->transform('class', $first); + $fixture->transform('class', $second); + $fixture->remove($transformation); + $this->assertEquals(['class' => [$second]], $fixture->transformations()); + } + + #[@test] + public function remove_unsets_empty_kind() { + $function= function($class) { return $class; }; + + $fixture= $this->newEmitter(); + $transformation= $fixture->transform('class', $function); + $fixture->remove($transformation); + $this->assertEquals([], $fixture->transformations()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index d6395a24..2eb4255e 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -16,6 +16,7 @@ abstract class EmittingTest extends TestCase { private static $cl, $language, $emitter; private static $id= 0; + private $transformations= []; #[@beforeClass] public static function setupCompiler() { @@ -27,8 +28,22 @@ public static function setupCompiler() { } } + /** @return void */ + public function tearDown() { + foreach ($this->transformations as $transformation) { + self::$emitter->remove($transformation); + } + } + + /** + * Register a transformation. Will take care of removing it on test shutdown. + * + * @param string $kind + * @param function(lang.ast.Node): lang.ast.Node|iterable $function + * @return void + */ protected function transform($type, $function) { - self::$emitter->transform($type, $function); + $this->transformations[]= self::$emitter->transform($type, $function); } /** diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 1778f706..0f3dafe8 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -1,6 +1,7 @@ transform('class', function($class) { + if ($class->annotation('repr')) { + $class->inject(new Method( + ['public'], + 'toString', + new Signature([], new Type('string')), + [new Code('return "T@".\util\Objects::stringOf(get_object_vars($this))')] + )); + } + return $class; + }); $this->transform('class', function($class) { if ($class->annotation('getters')) { foreach ($class->properties() as $property) { @@ -23,9 +35,29 @@ public function setUp() { }); } - /** @return void */ - public function tearDown() { - $this->transform('class', null); + #[@test] + public function leaves_class_without_annotations() { + $t= $this->type('class { + private int $id; + + public function __construct(int $id) { + $this->id= $id; + } + }'); + $this->assertFalse($t->hasMethod('id')); + } + + #[@test] + public function generates_string_representation() { + $t= $this->type('<> class { + private int $id; + + public function __construct(int $id) { + $this->id= $id; + } + }'); + $this->assertTrue($t->hasMethod('toString')); + $this->assertEquals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); } #[@test, @values([['id', 1], ['name', 'Test']])] @@ -42,4 +74,17 @@ public function __construct(int $id, string $name) { $this->assertTrue($t->hasMethod($name)); $this->assertEquals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); } + + #[@test] + public function generates_both() { + $t= $this->type('<> class { + private int $id; + + public function __construct(int $id) { + $this->id= $id; + } + }'); + $this->assertEquals(1, $t->getMethod('id')->invoke($t->newInstance(1))); + $this->assertEquals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); + } } \ No newline at end of file From 8103dcf936dba85b0625d63f4c1c2f8f82ee1feb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 13:26:46 +0200 Subject: [PATCH 099/926] Prevent extraneous semicolons after namespace and return statements --- src/main/php/lang/ast/Emitter.class.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index fc1942f6..1d642f24 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -37,7 +37,7 @@ public static function forRuntime($runtime) { * Returns a handle to remove the transformation again * * @param string $kind - * @param (function(lang.ast.Node): lang.ast.Node|iterable) $function + * @param function(lang.ast.Node): lang.ast.Node|iterable $function * @return var */ public function transform($kind, $function) { @@ -139,7 +139,7 @@ protected function emitStart($result, $start) { } protected function emitNamespace($result, $declaration) { - $result->out->write('namespace '.$declaration->name.";\n"); + $result->out->write('namespace '.$declaration->name."\n"); } protected function emitImport($result, $import) { @@ -566,7 +566,6 @@ protected function emitAssignment($result, $assignment) { protected function emitReturn($result, $return) { $result->out->write('return '); $return->expression && $this->emit($result, $return->expression); - $result->out->write(';'); } protected function emitIf($result, $if) { From e2397816e1a30f148a0a5bc0dc7b96176ac301f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 13:30:11 +0200 Subject: [PATCH 100/926] Require https://github.com/xp-framework/ast/releases/tag/v2.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9c2506bf..6af85669 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "dev-master as 2.0.0", + "xp-framework/ast": "^2.0", "php" : ">=5.6.0" }, "require-dev" : { From ae42bfb75a3089c1d1e2dc8b6459d80e516b0403 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 13:58:19 +0200 Subject: [PATCH 101/926] Add Extension interface --- src/main/php/lang/ast/syntax/Extension.class.php | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 src/main/php/lang/ast/syntax/Extension.class.php diff --git a/src/main/php/lang/ast/syntax/Extension.class.php b/src/main/php/lang/ast/syntax/Extension.class.php new file mode 100755 index 00000000..8cc3730b --- /dev/null +++ b/src/main/php/lang/ast/syntax/Extension.class.php @@ -0,0 +1,6 @@ + Date: Fri, 6 Sep 2019 13:58:31 +0200 Subject: [PATCH 102/926] Implement Extension interface --- src/main/php/lang/ast/Language.class.php | 3 ++- src/main/php/lang/ast/syntax/TransformationApi.class.php | 2 +- src/main/php/lang/ast/syntax/php/CompactMethods.class.php | 3 ++- src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php | 3 ++- src/main/php/lang/ast/syntax/php/NullSafe.class.php | 3 ++- src/main/php/lang/ast/syntax/php/Using.class.php | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 548b1930..da34135e 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -4,6 +4,7 @@ use lang\ast\nodes\BinaryExpression; use lang\ast\nodes\Literal; use lang\ast\nodes\UnaryExpression; +use lang\ast\syntax\Extension; use lang\ast\syntax\TransformationApi; use lang\reflect\Package; @@ -154,7 +155,7 @@ public function statements($parse) { public function extensions() { yield new TransformationApi(); foreach (Package::forName(strtr(strtolower(static::class), '\\', '.'))->getClasses() as $class) { - yield $class->newInstance(); + if ($class->isSubclassOf(Extension::class)) yield $class->newInstance(); } } diff --git a/src/main/php/lang/ast/syntax/TransformationApi.class.php b/src/main/php/lang/ast/syntax/TransformationApi.class.php index 92097e9e..1a41dfb6 100755 --- a/src/main/php/lang/ast/syntax/TransformationApi.class.php +++ b/src/main/php/lang/ast/syntax/TransformationApi.class.php @@ -2,7 +2,7 @@ use lang\ast\transform\Transformations; -class TransformationApi { +class TransformationApi implements Extension { public function setup($language, $emitter) { foreach (Transformations::registered() as $kind => $function) { diff --git a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php index 1c0e56d9..47382794 100755 --- a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php @@ -3,6 +3,7 @@ use lang\ast\Node; use lang\ast\nodes\Method; use lang\ast\nodes\ReturnStatement; +use lang\ast\syntax\Extension; /** * Compact functions @@ -23,7 +24,7 @@ * @see https://github.com/xp-framework/rfc/issues/241 * @test xp://lang.ast.unittest.emit.CompactFunctionsTest */ -class CompactMethods { +class CompactMethods implements Extension { public function setup($parser, $emitter) { $parser->body('fn', function($parse, &$body, $annotations, $modifiers) { diff --git a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php index b78c1a1a..f8a13157 100755 --- a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php +++ b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php @@ -3,6 +3,7 @@ use lang\ast\nodes\LambdaExpression; use lang\ast\nodes\Parameter; use lang\ast\nodes\Signature; +use lang\ast\syntax\Extension; /** * Hack-style arrow functions @@ -11,7 +12,7 @@ * @see https://docs.hhvm.com/hack/functions/anonymous-functions * @test xp://lang.ast.unittest.emit.LambdasTest */ -class HackArrowFunctions { +class HackArrowFunctions implements Extension { public function setup($parser, $emitter) { $parser->infix('==>', 80, function($parse, $node, $left) { diff --git a/src/main/php/lang/ast/syntax/php/NullSafe.class.php b/src/main/php/lang/ast/syntax/php/NullSafe.class.php index 30ed502d..347d9067 100755 --- a/src/main/php/lang/ast/syntax/php/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/php/NullSafe.class.php @@ -7,6 +7,7 @@ use lang\ast\nodes\Literal; use lang\ast\nodes\TernaryExpression; use lang\ast\nodes\Variable; +use lang\ast\syntax\Extension; /** * Null-safe instance operator @@ -22,7 +23,7 @@ * @see https://github.com/xp-framework/compiler/issues/9 * @test xp://lang.ast.unittest.emit.NullSafeTest */ -class NullSafe { +class NullSafe implements Extension { public function setup($language, $emitter) { $language->infix('?->', 80, function($parse, $node, $left) { diff --git a/src/main/php/lang/ast/syntax/php/Using.class.php b/src/main/php/lang/ast/syntax/php/Using.class.php index 947c6870..a666fae5 100755 --- a/src/main/php/lang/ast/syntax/php/Using.class.php +++ b/src/main/php/lang/ast/syntax/php/Using.class.php @@ -9,6 +9,7 @@ use lang\ast\nodes\TryStatement; use lang\ast\nodes\UsingStatement; use lang\ast\nodes\Variable; +use lang\ast\syntax\Extension; /** * Using statement @@ -33,7 +34,7 @@ * @see https://github.com/xp-framework/compiler/pull/33 * @test xp://lang.ast.unittest.emit.UsingTest */ -class Using { +class Using implements Extension { public function setup($language, $emitter) { $language->stmt('using', function($parse, $node) { From 760e8c53fca2bc36cfaa06839940b2e580e82db9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Sep 2019 14:34:42 +0200 Subject: [PATCH 103/926] QA: API documentation --- src/main/php/lang/ast/syntax/Extension.class.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/php/lang/ast/syntax/Extension.class.php b/src/main/php/lang/ast/syntax/Extension.class.php index 8cc3730b..403c0688 100755 --- a/src/main/php/lang/ast/syntax/Extension.class.php +++ b/src/main/php/lang/ast/syntax/Extension.class.php @@ -2,5 +2,12 @@ interface Extension { + /** + * Register this syntax extension with the language and emitter + * + * @param lang.ast.Language $language + * @param lang.ast.Emitter $emitter + * @return void + */ public function setup($language, $emitter); } \ No newline at end of file From 498cd0e2695c953613950438a0ef279f98ff063e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 13:12:19 +0200 Subject: [PATCH 104/926] Invoke test suite with "-a output" to show emitted code --- .../lang/ast/unittest/emit/EmittingTest.class.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 2eb4255e..4ee79584 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -12,10 +12,12 @@ use lang\ast\Tokens; use text\StringTokenizer; use unittest\TestCase; +use util\cmd\Console; abstract class EmittingTest extends TestCase { private static $cl, $language, $emitter; private static $id= 0; + private $output; private $transformations= []; #[@beforeClass] @@ -28,6 +30,11 @@ public static function setupCompiler() { } } + public function __construct($name, $output= null) { + parent::__construct($name); + $this->output= $output; + } + /** @return void */ public function tearDown() { foreach ($this->transformations as $transformation) { @@ -59,7 +66,11 @@ protected function type($code) { $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); self::$emitter->emitAll(new Result(new StringWriter($out)), $parse->execute()); - // var_dump($out->getBytes()); + if ($this->output) { + Console::writeLine(); + Console::writeLine('=== ', $this->name, ' ==='); + Console::writeLine($out->getBytes()); + } self::$cl->setClassBytes($name, $out->getBytes()); return self::$cl->loadClass($name); } From 9e86347a63decdb0575f7bdf2acc4518de474ded Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 13:17:07 +0200 Subject: [PATCH 105/926] Prevent endless loops should `$node->kind` be empty in `{emit.$node->kind}` --- src/main/php/lang/ast/Emitter.class.php | 141 ++++++++++-------- .../ast/emit/OmitConstModifiers.class.php | 2 +- src/main/php/lang/ast/emit/PHP56.class.php | 26 ++-- .../emit/RewriteLambdaExpressions.class.php | 2 +- .../RewriteNullCoalesceAssignment.class.php | 4 +- 5 files changed, 99 insertions(+), 76 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 1d642f24..170b5648 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -168,7 +168,7 @@ protected function emitEcho($result, $echo) { $result->out->write('echo '); $s= sizeof($echo->expressions) - 1; foreach ($echo->expressions as $i => $expr) { - $this->emit($result, $expr); + $this->emitOne($result, $expr); if ($i < $s) $result->out->write(','); } } @@ -184,7 +184,7 @@ protected function emitStatic($result, $static) { $result->out->write('static $'.$variable); if ($initial) { $result->out->write('='); - $this->emit($result, $initial); + $this->emitOne($result, $initial); } $result->out->write(';'); } @@ -200,14 +200,14 @@ protected function emitCast($result, $cast) { $name= $cast->type->name(); if ('?' === $name{0}) { $result->out->write('cast('); - $this->emit($result, $cast->expression); + $this->emitOne($result, $cast->expression); $result->out->write(',\''.$name.'\', false)'); } else if (isset($native[$name])) { $result->out->write('('.$cast->type->literal().')'); - $this->emit($result, $cast->expression); + $this->emitOne($result, $cast->expression); } else { $result->out->write('cast('); - $this->emit($result, $cast->expression); + $this->emitOne($result, $cast->expression); $result->out->write(',\''.$name.'\')'); } } @@ -230,22 +230,22 @@ protected function emitArray($result, $array) { $result->out->write('array_merge(['); foreach ($array->values as $pair) { if ($pair[0]) { - $this->emit($result, $pair[0]); + $this->emitOne($result, $pair[0]); $result->out->write('=>'); } if ('unpack' === $pair[1]->kind) { if ('array' === $pair[1]->expression->kind) { $result->out->write('],'); - $this->emit($result, $pair[1]->expression); + $this->emitOne($result, $pair[1]->expression); $result->out->write(',['); } else { $t= $result->temp(); $result->out->write('],('.$t.'='); - $this->emit($result, $pair[1]->expression); + $this->emitOne($result, $pair[1]->expression); $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); } } else { - $this->emit($result, $pair[1]); + $this->emitOne($result, $pair[1]); $result->out->write(','); } } @@ -254,10 +254,10 @@ protected function emitArray($result, $array) { $result->out->write('['); foreach ($array->values as $pair) { if ($pair[0]) { - $this->emit($result, $pair[0]); + $this->emitOne($result, $pair[0]); $result->out->write('=>'); } - $this->emit($result, $pair[1]); + $this->emitOne($result, $pair[1]); $result->out->write(','); } $result->out->write(']'); @@ -275,7 +275,7 @@ protected function emitParameter($result, $parameter) { } if ($parameter->default) { $result->out->write('='); - $this->emit($result, $parameter->default); + $this->emitOne($result, $parameter->default); } $result->locals[$parameter->name]= true; } @@ -338,7 +338,7 @@ protected function emitLambda($result, $lambda) { $this->emitAll($result, $lambda->body); $result->out->write('}'); } else { - $this->emit($result, $lambda->body); + $this->emitOne($result, $lambda->body); } } @@ -350,7 +350,7 @@ protected function emitClass($result, $class) { $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); $result->out->write('{'); foreach ($class->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); } $result->out->write('static function __init() {'); @@ -362,7 +362,7 @@ protected function emitAnnotations($result, $annotations) { foreach ($annotations as $name => $annotation) { $result->out->write("'".$name."' => "); if ($annotation) { - $this->emit($result, $annotation); + $this->emitOne($result, $annotation); $result->out->write(','); } else { $result->out->write('null,'); @@ -403,7 +403,7 @@ protected function emitInterface($result, $interface) { $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); $result->out->write('{'); foreach ($interface->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); $result->out->write("\n"); } $result->out->write('}'); @@ -417,7 +417,7 @@ protected function emitTrait($result, $trait) { $result->out->write('trait '.$this->declaration($trait->name)); $result->out->write('{'); foreach ($trait->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); $result->out->write("\n"); } @@ -441,7 +441,7 @@ protected function emitUse($result, $use) { protected function emitConst($result, $const) { $result->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); - $this->emit($result, $const->expression); + $this->emitOne($result, $const->expression); $result->out->write(';'); } @@ -457,7 +457,7 @@ protected function emitProperty($result, $property) { $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); if (isset($property->expression)) { $result->out->write('='); - $this->emit($result, $property->expression); + $this->emitOne($result, $property->expression); } $result->out->write(';'); } @@ -507,36 +507,36 @@ protected function emitMethod($result, $method) { protected function emitBraced($result, $braced) { $result->out->write('('); - $this->emit($result, $braced->expression); + $this->emitOne($result, $braced->expression); $result->out->write(')'); } protected function emitBinary($result, $binary) { - $this->emit($result, $binary->left); + $this->emitOne($result, $binary->left); $result->out->write(' '.$binary->operator.' '); - $this->emit($result, $binary->right); + $this->emitOne($result, $binary->right); } protected function emitUnary($result, $unary) { $result->out->write($unary->operator); - $this->emit($result, $unary->expression); + $this->emitOne($result, $unary->expression); } protected function emitTernary($result, $ternary) { - $this->emit($result, $ternary->condition); + $this->emitOne($result, $ternary->condition); $result->out->write('?'); - $this->emit($result, $ternary->expression); + $this->emitOne($result, $ternary->expression); $result->out->write(':'); - $this->emit($result, $ternary->otherwise); + $this->emitOne($result, $ternary->otherwise); } protected function emitOffset($result, $offset) { - $this->emit($result, $offset->expression); + $this->emitOne($result, $offset->expression); if (null === $offset->offset) { $result->out->write('[]'); } else { $result->out->write('['); - $this->emit($result, $offset->offset); + $this->emitOne($result, $offset->offset); $result->out->write(']'); } } @@ -553,24 +553,24 @@ protected function emitAssign($result, $target) { } $result->out->write(')'); } else { - $this->emit($result, $target); + $this->emitOne($result, $target); } } protected function emitAssignment($result, $assignment) { $this->emitAssign($result, $assignment->variable); $result->out->write($assignment->operator); - $this->emit($result, $assignment->expression); + $this->emitOne($result, $assignment->expression); } protected function emitReturn($result, $return) { $result->out->write('return '); - $return->expression && $this->emit($result, $return->expression); + $return->expression && $this->emitOne($result, $return->expression); } protected function emitIf($result, $if) { $result->out->write('if ('); - $this->emit($result, $if->expression); + $this->emitOne($result, $if->expression); $result->out->write(') {'); $this->emitAll($result, $if->body); $result->out->write('}'); @@ -584,17 +584,17 @@ protected function emitIf($result, $if) { protected function emitSwitch($result, $switch) { $result->out->write('switch ('); - $this->emit($result, $switch->expression); + $this->emitOne($result, $switch->expression); $result->out->write(') {'); foreach ($switch->cases as $case) { if ($case->expression) { $result->out->write('case '); - $this->emit($result, $case->expression); + $this->emitOne($result, $case->expression); $result->out->write(':'); } else { $result->out->write('default:'); } - $this->emit($result, $case->body); + $this->emitOne($result, $case->body); } $result->out->write('}'); } @@ -627,7 +627,7 @@ protected function emitTry($result, $try) { protected function emitThrow($result, $throw) { $result->out->write('throw '); - $this->emit($result, $throw->expression); + $this->emitOne($result, $throw->expression); $result->out->write(';'); } @@ -643,19 +643,19 @@ protected function emitThrowExpression($result, $throw) { $result->out->write('(function()'); $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); $result->out->write('{ throw '); - $this->emit($result, $throw->expression); + $this->emitOne($result, $throw->expression); $result->out->write('; })()'); } protected function emitForeach($result, $foreach) { $result->out->write('foreach ('); - $this->emit($result, $foreach->expression); + $this->emitOne($result, $foreach->expression); $result->out->write(' as '); if ($foreach->key) { - $this->emit($result, $foreach->key); + $this->emitOne($result, $foreach->key); $result->out->write(' => '); } - $this->emit($result, $foreach->value); + $this->emitOne($result, $foreach->value); $result->out->write(') {'); $this->emitAll($result, $foreach->body); $result->out->write('}'); @@ -678,13 +678,13 @@ protected function emitDo($result, $do) { $result->out->write('{'); $this->emitAll($result, $do->body); $result->out->write('} while ('); - $this->emit($result, $do->expression); + $this->emitOne($result, $do->expression); $result->out->write(');'); } protected function emitWhile($result, $while) { $result->out->write('while ('); - $this->emit($result, $while->expression); + $this->emitOne($result, $while->expression); $result->out->write(') {'); $this->emitAll($result, $while->body); $result->out->write('}'); @@ -692,13 +692,13 @@ protected function emitWhile($result, $while) { protected function emitBreak($result, $break) { $result->out->write('break '); - $break->expression && $this->emit($result, $break->expression); + $break->expression && $this->emitOne($result, $break->expression); $result->out->write(';'); } protected function emitContinue($result, $continue) { $result->out->write('continue '); - $continue->expression && $this->emit($result, $continue->expression); + $continue->expression && $this->emitOne($result, $continue->expression); $result->out->write(';'); } @@ -711,10 +711,10 @@ protected function emitGoto($result, $goto) { } protected function emitInstanceOf($result, $instanceof) { - $this->emit($result, $instanceof->expression); + $this->emitOne($result, $instanceof->expression); $result->out->write(' instanceof '); if ($instanceof->type instanceof Value) { - $this->emit($result, $instanceof->type); + $this->emitOne($result, $instanceof->type); } else { $result->out->write($instanceof->type); } @@ -723,7 +723,7 @@ protected function emitInstanceOf($result, $instanceof) { protected function emitArguments($result, $arguments) { $s= sizeof($arguments) - 1; foreach ($arguments as $i => $argument) { - $this->emit($result, $argument); + $this->emitOne($result, $argument); if ($i < $s) $result->out->write(', '); } } @@ -743,14 +743,14 @@ protected function emitNewClass($result, $new) { $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); $result->out->write('{'); foreach ($new->definition->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); $result->out->write("\n"); } $result->out->write('}'); } protected function emitInvoke($result, $invoke) { - $this->emit($result, $invoke->expression); + $this->emitOne($result, $invoke->expression); $result->out->write('('); $this->emitArguments($result, $invoke->arguments); $result->out->write(')'); @@ -758,16 +758,16 @@ protected function emitInvoke($result, $invoke) { protected function emitScope($result, $scope) { $result->out->write($scope->type.'::'); - $this->emit($result, $scope->member); + $this->emitOne($result, $scope->member); } protected function emitInstance($result, $instance) { if ('new' === $instance->expression->kind) { $result->out->write('('); - $this->emit($result, $instance->expression); + $this->emitOne($result, $instance->expression); $result->out->write(')->'); } else { - $this->emit($result, $instance->expression); + $this->emitOne($result, $instance->expression); $result->out->write('->'); } @@ -775,40 +775,63 @@ protected function emitInstance($result, $instance) { $result->out->write($instance->member->expression); } else { $result->out->write('{'); - $this->emit($result, $instance->member); + $this->emitOne($result, $instance->member); $result->out->write('}'); } } protected function emitUnpack($result, $unpack) { $result->out->write('...'); - $this->emit($result, $unpack->expression); + $this->emitOne($result, $unpack->expression); } protected function emitYield($result, $yield) { $result->out->write('yield '); if ($yield->key) { - $this->emit($result, $yield->key); + $this->emitOne($result, $yield->key); $result->out->write('=>'); } if ($yield->value) { - $this->emit($result, $yield->value); + $this->emitOne($result, $yield->value); } } protected function emitFrom($result, $from) { $result->out->write('yield from '); - $this->emit($result, $from->iterable); + $this->emitOne($result, $from->iterable); } + /** + * Catch-all, should `$node->kind` be empty in `{'emit'.$node->kind}`. + * + * @return void + */ + public function emit() { + throw new IllegalStateException('Called without node kind'); + } + + /** + * Emit nodes seperated as statements + * + * @param lang.ast.Result $result + * @param iterable $nodes + * @return void + */ public function emitAll($result, $nodes) { foreach ($nodes as $node) { - $this->emit($result, $node); + $this->emitOne($result, $node); $result->out->write(';'); } } - public function emit($result, $node) { + /** + * Emit single nodes + * + * @param lang.ast.Result $result + * @param lang.ast.Element $node + * @return void + */ + public function emitOne($result, $node) { if ($node->line > $result->line) { $result->out->write(str_repeat("\n", $node->line - $result->line)); $result->line= $node->line; diff --git a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php index 8cb8f2a8..2f3a69c7 100755 --- a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php +++ b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php @@ -10,7 +10,7 @@ trait OmitConstModifiers { protected function emitConst($result, $const) { $result->out->write('const '.$const->name.'='); - $this->emit($result, $const->expression); + $this->emitOne($result, $const->expression); $result->out->write(';'); } } diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 86207491..eab53629 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -121,18 +121,18 @@ protected function emitCatch($result, $catch) { protected function emitBinary($result, $binary) { if ('??' === $binary->operator) { $result->out->write('isset('); - $this->emit($result, $binary->left); + $this->emitOne($result, $binary->left); $result->out->write(') ?'); - $this->emit($result, $binary->left); + $this->emitOne($result, $binary->left); $result->out->write(' : '); - $this->emit($result, $binary->right); + $this->emitOne($result, $binary->right); } else if ('<=>' === $binary->operator) { $l= $result->temp(); $r= $result->temp(); $result->out->write('('.$l.'= '); - $this->emit($result, $binary->left); + $this->emitOne($result, $binary->left); $result->out->write(') < ('.$r.'='); - $this->emit($result, $binary->right); + $this->emitOne($result, $binary->right); $result->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); } else { parent::emitBinary($result, $binary); @@ -144,13 +144,13 @@ protected function emitAssignment($result, $assignment) { $result->out->write('isset('); $this->emitAssign($result, $assignment->variable); $result->out->write(') ||'); - $this->emit($result, $assignment->variable); + $this->emitOne($result, $assignment->variable); $result->out->write('='); - $this->emit($result, $assignment->expression); + $this->emitOne($result, $assignment->expression); } else { $this->emitAssign($result, $assignment->variable); $result->out->write($assignment->operator); - $this->emit($result, $assignment->expression); + $this->emitOne($result, $assignment->expression); } } @@ -160,7 +160,7 @@ protected function emitInvoke($result, $invoke) { if ('braced' === $expr->kind) { $t= $result->temp(); $result->out->write('(('.$t.'='); - $this->emit($result, $expr->expression); + $this->emitOne($result, $expr->expression); $result->out->write(') ? '.$t); $result->out->write('('); $this->emitArguments($result, $invoke->arguments); @@ -193,7 +193,7 @@ protected function emitThrowExpression($result, $throw) { $result->out->write('(('.$t.'=function()'); $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); $result->out->write('{ throw '); - $this->emit($result, $throw->expression); + $this->emitOne($result, $throw->expression); $result->out->write('; }) ? '.$t.'() : null)'); } @@ -206,7 +206,7 @@ protected function emitNewClass($result, $new) { $result->out->write('], \'{'); $result->out->write(str_replace('\'', '\\\'', $result->buffer(function($result) use($definition) { foreach ($definition->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); $result->out->write("\n"); } }))); @@ -217,7 +217,7 @@ protected function emitNewClass($result, $new) { protected function emitFrom($result, $from) { $result->out->write('foreach ('); - $this->emit($result, $from->iterable); + $this->emitOne($result, $from->iterable); $result->out->write(' as $key => $val) yield $key => $val;'); } @@ -240,7 +240,7 @@ protected function emitClass($result, $class) { $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); $result->out->write('{'); foreach ($class->body as $member) { - $this->emit($result, $member); + $this->emitOne($result, $member); } if ($result->call[false]) { diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index 912cbb1b..ddb357cf 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -38,7 +38,7 @@ protected function emitLambda($result, $lambda) { $result->out->write('}'); } else { $result->out->write('{ return '); - $this->emit($result, $lambda->body); + $this->emitOne($result, $lambda->body); $result->out->write('; }'); } diff --git a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php index d19f341c..95288555 100755 --- a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php +++ b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php @@ -12,9 +12,9 @@ protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { $this->emitAssign($result, $assignment->variable); $result->out->write('='); - $this->emit($result, $assignment->variable); + $this->emitOne($result, $assignment->variable); $result->out->write('??'); - $this->emit($result, $assignment->expression); + $this->emitOne($result, $assignment->expression); } else { parent::emitAssignment($result, $assignment); } From 52b1e093e75022b32d5976048d21f371497b9a92 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 13:27:50 +0200 Subject: [PATCH 106/926] Allow dumping AST, too --- .../ast/unittest/emit/EmittingTest.class.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 4ee79584..347e9276 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -30,9 +30,15 @@ public static function setupCompiler() { } } + /** + * Constructor + * + * @param string $name + * @param ?string $output E.g. `ast,code` to dump both AST and emitted code + */ public function __construct($name, $output= null) { parent::__construct($name); - $this->output= $output; + $this->output= $output ? array_flip(explode(',', $output)) : []; } /** @return void */ @@ -64,13 +70,20 @@ protected function type($code) { $out= new MemoryOutputStream(); $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); - self::$emitter->emitAll(new Result(new StringWriter($out)), $parse->execute()); + $ast= iterator_to_array($parse->execute()); + if (isset($this->output['ast'])) { + Console::writeLine(); + Console::writeLine('=== ', $this->name, ' ==='); + Console::writeLine($ast); + } - if ($this->output) { + self::$emitter->emitAll(new Result(new StringWriter($out)), $ast); + if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', $this->name, ' ==='); Console::writeLine($out->getBytes()); } + self::$cl->setClassBytes($name, $out->getBytes()); return self::$cl->loadClass($name); } From e24857e67e24fd517aa0d574b45ea4b334141cfd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 13:31:27 +0200 Subject: [PATCH 107/926] Verify emit() catch-call functionality --- src/main/php/lang/ast/Emitter.class.php | 1 + src/test/php/lang/ast/unittest/EmitterTest.class.php | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 170b5648..f554457f 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,6 +1,7 @@ remove($transformation); $this->assertEquals([], $fixture->transformations()); } + + #[@test, @expect(IllegalStateException::class)] + public function emit_element_without_kind() { + $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), newinstance(Element::class, [], [ + 'line' => -1, + 'kind' => null + ])); + } } \ No newline at end of file From b438555b9ba40e7e7cab19293191849513214264 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 13:37:43 +0200 Subject: [PATCH 108/926] Extract PHP code emitter from base class --- src/main/php/lang/ast/Emitter.class.php | 717 -------------------- src/main/php/lang/ast/emit/PHP.class.php | 724 +++++++++++++++++++++ src/main/php/lang/ast/emit/PHP56.class.php | 5 +- src/main/php/lang/ast/emit/PHP70.class.php | 4 +- src/main/php/lang/ast/emit/PHP71.class.php | 4 +- src/main/php/lang/ast/emit/PHP72.class.php | 4 +- src/main/php/lang/ast/emit/PHP74.class.php | 4 +- src/main/php/lang/ast/emit/PHP80.class.php | 4 +- 8 files changed, 730 insertions(+), 736 deletions(-) create mode 100755 src/main/php/lang/ast/emit/PHP.class.php diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index f554457f..6a1c315c 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -6,10 +6,6 @@ use lang\reflect\Package; abstract class Emitter { - const PROPERTY = 0; - const METHOD = 1; - - protected $unsupported= []; private $transformations= []; /** @@ -73,31 +69,6 @@ public function transformations() { return $this->transformations; } - /** - * Returns the simple name for use in a declaration - * - * @param string $name E.g. `\lang\ast\Parse` - * @return string In the above example, `Parse`. - */ - protected function declaration($name) { - return substr($name, strrpos($name, '\\') + 1); - } - - /** - * Returns type literal or NULL - * - * @param string $name - * @return string - */ - protected function type($name) { - return ( - '?' === $name{0} || // nullable - 0 === strncmp($name, 'function', 8) || // function - strstr($name, '|') || // union - isset($this->unsupported[$name]) - ) ? null : $name; - } - /** * Search a given scope recursively for nodes with a given kind * @@ -114,694 +85,6 @@ protected function search($arg, $kind) { } } - protected function paramType($type) { - return $this->type($type->literal()); - } - - protected function returnType($type) { - return $this->type($type->literal()); - } - - // See https://wiki.php.net/rfc/typed_properties_v2#supported_types - protected function propertyType($type) { - if (null === $type || $type instanceof UnionType || $type instanceof FunctionType) { - return ''; - } else if ($type instanceof ArrayType || $type instanceof MapType) { - return 'array'; - } else if ($type instanceof Type && 'callable' !== $type->literal() && 'void' !== $type->literal()) { - return $type->literal(); - } else { - return ''; - } - } - - protected function emitStart($result, $start) { - $result->out->write('out->write('namespace '.$declaration->name."\n"); - } - - protected function emitImport($result, $import) { - foreach ($import->names as $name => $alias) { - $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitAnnotation($result, $annotations) { - // NOOP - } - - protected function emitCode($result, $code) { - $result->out->write($code->value); - } - - protected function emitLiteral($result, $literal) { - $result->out->write($literal->expression); - } - - protected function emitName($result, $name) { - $result->out->write($name); - } - - protected function emitEcho($result, $echo) { - $result->out->write('echo '); - $s= sizeof($echo->expressions) - 1; - foreach ($echo->expressions as $i => $expr) { - $this->emitOne($result, $expr); - if ($i < $s) $result->out->write(','); - } - } - - protected function emitBlock($result, $block) { - $result->out->write('{'); - $this->emitAll($result, $block->statements); - $result->out->write('}'); - } - - protected function emitStatic($result, $static) { - foreach ($static->initializations as $variable => $initial) { - $result->out->write('static $'.$variable); - if ($initial) { - $result->out->write('='); - $this->emitOne($result, $initial); - } - $result->out->write(';'); - } - } - - protected function emitVariable($result, $variable) { - $result->out->write('$'.$variable->name); - } - - protected function emitCast($result, $cast) { - static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; - - $name= $cast->type->name(); - if ('?' === $name{0}) { - $result->out->write('cast('); - $this->emitOne($result, $cast->expression); - $result->out->write(',\''.$name.'\', false)'); - } else if (isset($native[$name])) { - $result->out->write('('.$cast->type->literal().')'); - $this->emitOne($result, $cast->expression); - } else { - $result->out->write('cast('); - $this->emitOne($result, $cast->expression); - $result->out->write(',\''.$name.'\')'); - } - } - - protected function emitArray($result, $array) { - if (empty($array->values)) { - $result->out->write('[]'); - return; - } - - $unpack= false; - foreach ($array->values as $pair) { - if ('unpack' === $pair[1]->kind) { - $unpack= true; - break; - } - } - - if ($unpack) { - $result->out->write('array_merge(['); - foreach ($array->values as $pair) { - if ($pair[0]) { - $this->emitOne($result, $pair[0]); - $result->out->write('=>'); - } - if ('unpack' === $pair[1]->kind) { - if ('array' === $pair[1]->expression->kind) { - $result->out->write('],'); - $this->emitOne($result, $pair[1]->expression); - $result->out->write(',['); - } else { - $t= $result->temp(); - $result->out->write('],('.$t.'='); - $this->emitOne($result, $pair[1]->expression); - $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); - } - } else { - $this->emitOne($result, $pair[1]); - $result->out->write(','); - } - } - $result->out->write('])'); - } else { - $result->out->write('['); - foreach ($array->values as $pair) { - if ($pair[0]) { - $this->emitOne($result, $pair[0]); - $result->out->write('=>'); - } - $this->emitOne($result, $pair[1]); - $result->out->write(','); - } - $result->out->write(']'); - } - } - - protected function emitParameter($result, $parameter) { - if ($parameter->type && $t= $this->paramType($parameter->type)) { - $result->out->write($t.' '); - } - if ($parameter->variadic) { - $result->out->write('... $'.$parameter->name); - } else { - $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); - } - if ($parameter->default) { - $result->out->write('='); - $this->emitOne($result, $parameter->default); - } - $result->locals[$parameter->name]= true; - } - - protected function emitSignature($result, $signature) { - $result->out->write('('); - $s= sizeof($signature->parameters) - 1; - foreach ($signature->parameters as $i => $parameter) { - $this->emitParameter($result, $parameter); - if ($i < $s) $result->out->write(', '); - } - $result->out->write(')'); - - if ($signature->returns && $t= $this->returnType($signature->returns)) { - $result->out->write(':'.$t); - } - } - - protected function emitFunction($result, $function) { - $result->stack[]= $result->locals; - $result->locals= []; - - $result->out->write('function '.$function->name); - $this->emitSignature($result, $function->signature); - - $result->out->write('{'); - $this->emitAll($result, $function->body); - $result->out->write('}'); - - $result->locals= array_pop($result->stack); - } - - protected function emitClosure($result, $closure) { - $result->stack[]= $result->locals; - $result->locals= []; - - $result->out->write('function'); - $this->emitSignature($result, $closure->signature); - - if ($closure->use) { - $result->out->write(' use('.implode(',', $closure->use).') '); - foreach ($closure->use as $variable) { - $result->locals[substr($variable, 1)]= true; - } - } - $result->out->write('{'); - $this->emitAll($result, $closure->body); - $result->out->write('}'); - - $result->locals= array_pop($result->stack); - } - - protected function emitLambda($result, $lambda) { - $result->out->write('fn'); - $this->emitSignature($result, $lambda->signature); - $result->out->write('=>'); - - if (is_array($lambda->body)) { - $result->out->write('{'); - $this->emitAll($result, $lambda->body); - $result->out->write('}'); - } else { - $this->emitOne($result, $lambda->body); - } - } - - protected function emitClass($result, $class) { - array_unshift($result->meta, []); - - $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $result->out->write(' extends '.$class->parent); - $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); - $result->out->write('{'); - foreach ($class->body as $member) { - $this->emitOne($result, $member); - } - - $result->out->write('static function __init() {'); - $this->emitMeta($result, $class->name, $class->annotations, $class->comment); - $result->out->write('}} '.$class->name.'::__init();'); - } - - protected function emitAnnotations($result, $annotations) { - foreach ($annotations as $name => $annotation) { - $result->out->write("'".$name."' => "); - if ($annotation) { - $this->emitOne($result, $annotation); - $result->out->write(','); - } else { - $result->out->write('null,'); - } - } - } - - protected function emitMeta($result, $name, $annotations, $comment) { - $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); - $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); - $this->emitAnnotations($result, $annotations); - $result->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); - - foreach (array_shift($result->meta) as $type => $lookup) { - $result->out->write($type.' => ['); - foreach ($lookup as $key => $meta) { - $result->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); - $this->emitAnnotations($result, $meta[DETAIL_ANNOTATIONS]); - $result->out->write('], DETAIL_TARGET_ANNO => ['); - foreach ($meta[DETAIL_TARGET_ANNO] as $target => $annotations) { - $result->out->write("'$".$target."' => ["); - $this->emitAnnotations($result, $annotations); - $result->out->write('],'); - } - $result->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); - $result->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); - $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); - } - $result->out->write('],'); - } - $result->out->write('];'); - } - - protected function emitInterface($result, $interface) { - array_unshift($result->meta, []); - - $result->out->write('interface '.$this->declaration($interface->name)); - $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); - $result->out->write('{'); - foreach ($interface->body as $member) { - $this->emitOne($result, $member); - $result->out->write("\n"); - } - $result->out->write('}'); - - $this->emitMeta($result, $interface->name, $interface->annotations, $interface->comment); - } - - protected function emitTrait($result, $trait) { - array_unshift($result->meta, []); - - $result->out->write('trait '.$this->declaration($trait->name)); - $result->out->write('{'); - foreach ($trait->body as $member) { - $this->emitOne($result, $member); - $result->out->write("\n"); - } - - $result->out->write('static function __init() {'); - $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); - $result->out->write('}} '.$trait->name.'::__init();'); - } - - protected function emitUse($result, $use) { - $result->out->write('use '.implode(',', $use->types)); - if ($use->aliases) { - $result->out->write('{'); - foreach ($use->aliases as $reference => $alias) { - $result->out->write($reference.' as '.$alias.';'); - } - $result->out->write('}'); - } else { - $result->out->write(';'); - } - } - - protected function emitConst($result, $const) { - $result->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); - $this->emitOne($result, $const->expression); - $result->out->write(';'); - } - - protected function emitProperty($result, $property) { - $result->meta[0][self::PROPERTY][$property->name]= [ - DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', - DETAIL_ANNOTATIONS => $property->annotations ? $property->annotations : [], - DETAIL_COMMENT => $property->comment, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - - $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); - if (isset($property->expression)) { - $result->out->write('='); - $this->emitOne($result, $property->expression); - } - $result->out->write(';'); - } - - protected function emitMethod($result, $method) { - $result->stack[]= $result->locals; - $result->locals= ['this' => true]; - $meta= [ - DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', - DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], - DETAIL_COMMENT => $method->comment, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - - $declare= $promote= $params= ''; - foreach ($method->signature->parameters as $param) { - if (isset($param->promote)) { - $declare.= $param->promote.' $'.$param->name.';'; - $promote.= '$this->'.$param->name.'= $'.$param->name.';'; - $result->meta[0][self::PROPERTY][$param->name]= [ - DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', - DETAIL_ANNOTATIONS => [], - DETAIL_COMMENT => null, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - } - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; - $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; - } - $result->out->write($declare); - $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $this->emitSignature($result, $method->signature); - - if (null === $method->body) { - $result->out->write(';'); - } else { - $result->out->write(' {'.$promote); - $this->emitAll($result, $method->body); - $result->out->write('}'); - } - - $result->meta[0][self::METHOD][$method->name]= $meta; - $result->locals= array_pop($result->stack); - } - - protected function emitBraced($result, $braced) { - $result->out->write('('); - $this->emitOne($result, $braced->expression); - $result->out->write(')'); - } - - protected function emitBinary($result, $binary) { - $this->emitOne($result, $binary->left); - $result->out->write(' '.$binary->operator.' '); - $this->emitOne($result, $binary->right); - } - - protected function emitUnary($result, $unary) { - $result->out->write($unary->operator); - $this->emitOne($result, $unary->expression); - } - - protected function emitTernary($result, $ternary) { - $this->emitOne($result, $ternary->condition); - $result->out->write('?'); - $this->emitOne($result, $ternary->expression); - $result->out->write(':'); - $this->emitOne($result, $ternary->otherwise); - } - - protected function emitOffset($result, $offset) { - $this->emitOne($result, $offset->expression); - if (null === $offset->offset) { - $result->out->write('[]'); - } else { - $result->out->write('['); - $this->emitOne($result, $offset->offset); - $result->out->write(']'); - } - } - - protected function emitAssign($result, $target) { - if ('variable' === $target->kind) { - $result->out->write('$'.$target->name); - $result->locals[$target->name]= true; - } else if ('array' === $target->kind) { - $result->out->write('list('); - foreach ($target->values as $pair) { - $this->emitAssign($result, $pair[1]); - $result->out->write(','); - } - $result->out->write(')'); - } else { - $this->emitOne($result, $target); - } - } - - protected function emitAssignment($result, $assignment) { - $this->emitAssign($result, $assignment->variable); - $result->out->write($assignment->operator); - $this->emitOne($result, $assignment->expression); - } - - protected function emitReturn($result, $return) { - $result->out->write('return '); - $return->expression && $this->emitOne($result, $return->expression); - } - - protected function emitIf($result, $if) { - $result->out->write('if ('); - $this->emitOne($result, $if->expression); - $result->out->write(') {'); - $this->emitAll($result, $if->body); - $result->out->write('}'); - - if ($if->otherwise) { - $result->out->write('else {'); - $this->emitAll($result, $if->otherwise); - $result->out->write('}'); - } - } - - protected function emitSwitch($result, $switch) { - $result->out->write('switch ('); - $this->emitOne($result, $switch->expression); - $result->out->write(') {'); - foreach ($switch->cases as $case) { - if ($case->expression) { - $result->out->write('case '); - $this->emitOne($result, $case->expression); - $result->out->write(':'); - } else { - $result->out->write('default:'); - } - $this->emitOne($result, $case->body); - } - $result->out->write('}'); - } - - protected function emitCatch($result, $catch) { - if (empty($catch->types)) { - $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); - } else { - $result->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); - } - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } - - protected function emitTry($result, $try) { - $result->out->write('try {'); - $this->emitAll($result, $try->body); - $result->out->write('}'); - if (isset($try->catches)) { - foreach ($try->catches as $catch) { - $this->emitCatch($result, $catch); - } - } - if (isset($try->finally)) { - $result->out->write('finally {'); - $this->emitAll($result, $try->finally); - $result->out->write('}'); - } - } - - protected function emitThrow($result, $throw) { - $result->out->write('throw '); - $this->emitOne($result, $throw->expression); - $result->out->write(';'); - } - - protected function emitThrowExpression($result, $throw) { - $capture= []; - foreach ($this->search($throw->expression, 'variable') as $var) { - if (isset($result->locals[$var->name])) { - $capture[$var->name]= true; - } - } - unset($capture['this']); - - $result->out->write('(function()'); - $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $result->out->write('{ throw '); - $this->emitOne($result, $throw->expression); - $result->out->write('; })()'); - } - - protected function emitForeach($result, $foreach) { - $result->out->write('foreach ('); - $this->emitOne($result, $foreach->expression); - $result->out->write(' as '); - if ($foreach->key) { - $this->emitOne($result, $foreach->key); - $result->out->write(' => '); - } - $this->emitOne($result, $foreach->value); - $result->out->write(') {'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); - } - - protected function emitFor($result, $for) { - $result->out->write('for ('); - $this->emitArguments($result, $for->initialization); - $result->out->write(';'); - $this->emitArguments($result, $for->condition); - $result->out->write(';'); - $this->emitArguments($result, $for->loop); - $result->out->write(') {'); - $this->emitAll($result, $for->body); - $result->out->write('}'); - } - - protected function emitDo($result, $do) { - $result->out->write('do'); - $result->out->write('{'); - $this->emitAll($result, $do->body); - $result->out->write('} while ('); - $this->emitOne($result, $do->expression); - $result->out->write(');'); - } - - protected function emitWhile($result, $while) { - $result->out->write('while ('); - $this->emitOne($result, $while->expression); - $result->out->write(') {'); - $this->emitAll($result, $while->body); - $result->out->write('}'); - } - - protected function emitBreak($result, $break) { - $result->out->write('break '); - $break->expression && $this->emitOne($result, $break->expression); - $result->out->write(';'); - } - - protected function emitContinue($result, $continue) { - $result->out->write('continue '); - $continue->expression && $this->emitOne($result, $continue->expression); - $result->out->write(';'); - } - - protected function emitLabel($result, $label) { - $result->out->write($label->name.':'); - } - - protected function emitGoto($result, $goto) { - $result->out->write('goto '.$goto->label); - } - - protected function emitInstanceOf($result, $instanceof) { - $this->emitOne($result, $instanceof->expression); - $result->out->write(' instanceof '); - if ($instanceof->type instanceof Value) { - $this->emitOne($result, $instanceof->type); - } else { - $result->out->write($instanceof->type); - } - } - - protected function emitArguments($result, $arguments) { - $s= sizeof($arguments) - 1; - foreach ($arguments as $i => $argument) { - $this->emitOne($result, $argument); - if ($i < $s) $result->out->write(', '); - } - } - - protected function emitNew($result, $new) { - $result->out->write('new '.$new->type.'('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - } - - protected function emitNewClass($result, $new) { - $result->out->write('new class('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - - $new->definition->parent && $result->out->write(' extends '.$new->definition->parent); - $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); - $result->out->write('{'); - foreach ($new->definition->body as $member) { - $this->emitOne($result, $member); - $result->out->write("\n"); - } - $result->out->write('}'); - } - - protected function emitInvoke($result, $invoke) { - $this->emitOne($result, $invoke->expression); - $result->out->write('('); - $this->emitArguments($result, $invoke->arguments); - $result->out->write(')'); - } - - protected function emitScope($result, $scope) { - $result->out->write($scope->type.'::'); - $this->emitOne($result, $scope->member); - } - - protected function emitInstance($result, $instance) { - if ('new' === $instance->expression->kind) { - $result->out->write('('); - $this->emitOne($result, $instance->expression); - $result->out->write(')->'); - } else { - $this->emitOne($result, $instance->expression); - $result->out->write('->'); - } - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } - } - - protected function emitUnpack($result, $unpack) { - $result->out->write('...'); - $this->emitOne($result, $unpack->expression); - } - - protected function emitYield($result, $yield) { - $result->out->write('yield '); - if ($yield->key) { - $this->emitOne($result, $yield->key); - $result->out->write('=>'); - } - if ($yield->value) { - $this->emitOne($result, $yield->value); - } - } - - protected function emitFrom($result, $from) { - $result->out->write('yield from '); - $this->emitOne($result, $from->iterable); - } - /** * Catch-all, should `$node->kind` be empty in `{'emit'.$node->kind}`. * diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php new file mode 100755 index 00000000..47a7040e --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -0,0 +1,724 @@ +unsupported[$name]) + ) ? null : $name; + } + + protected function paramType($type) { + return $this->type($type->literal()); + } + + protected function returnType($type) { + return $this->type($type->literal()); + } + + // See https://wiki.php.net/rfc/typed_properties_v2#supported_types + protected function propertyType($type) { + if (null === $type || $type instanceof UnionType || $type instanceof FunctionType) { + return ''; + } else if ($type instanceof ArrayType || $type instanceof MapType) { + return 'array'; + } else if ($type instanceof Type && 'callable' !== $type->literal() && 'void' !== $type->literal()) { + return $type->literal(); + } else { + return ''; + } + } + + protected function emitStart($result, $start) { + $result->out->write('out->write('namespace '.$declaration->name."\n"); + } + + protected function emitImport($result, $import) { + foreach ($import->names as $name => $alias) { + $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';'); + } + } + + protected function emitAnnotation($result, $annotations) { + // NOOP + } + + protected function emitCode($result, $code) { + $result->out->write($code->value); + } + + protected function emitLiteral($result, $literal) { + $result->out->write($literal->expression); + } + + protected function emitName($result, $name) { + $result->out->write($name); + } + + protected function emitEcho($result, $echo) { + $result->out->write('echo '); + $s= sizeof($echo->expressions) - 1; + foreach ($echo->expressions as $i => $expr) { + $this->emitOne($result, $expr); + if ($i < $s) $result->out->write(','); + } + } + + protected function emitBlock($result, $block) { + $result->out->write('{'); + $this->emitAll($result, $block->statements); + $result->out->write('}'); + } + + protected function emitStatic($result, $static) { + foreach ($static->initializations as $variable => $initial) { + $result->out->write('static $'.$variable); + if ($initial) { + $result->out->write('='); + $this->emitOne($result, $initial); + } + $result->out->write(';'); + } + } + + protected function emitVariable($result, $variable) { + $result->out->write('$'.$variable->name); + } + + protected function emitCast($result, $cast) { + static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; + + $name= $cast->type->name(); + if ('?' === $name{0}) { + $result->out->write('cast('); + $this->emitOne($result, $cast->expression); + $result->out->write(',\''.$name.'\', false)'); + } else if (isset($native[$name])) { + $result->out->write('('.$cast->type->literal().')'); + $this->emitOne($result, $cast->expression); + } else { + $result->out->write('cast('); + $this->emitOne($result, $cast->expression); + $result->out->write(',\''.$name.'\')'); + } + } + + protected function emitArray($result, $array) { + if (empty($array->values)) { + $result->out->write('[]'); + return; + } + + $unpack= false; + foreach ($array->values as $pair) { + if ('unpack' === $pair[1]->kind) { + $unpack= true; + break; + } + } + + if ($unpack) { + $result->out->write('array_merge(['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + if ('unpack' === $pair[1]->kind) { + if ('array' === $pair[1]->expression->kind) { + $result->out->write('],'); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(',['); + } else { + $t= $result->temp(); + $result->out->write('],('.$t.'='); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); + } + } else { + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + } + $result->out->write('])'); + } else { + $result->out->write('['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(']'); + } + } + + protected function emitParameter($result, $parameter) { + if ($parameter->type && $t= $this->paramType($parameter->type)) { + $result->out->write($t.' '); + } + if ($parameter->variadic) { + $result->out->write('... $'.$parameter->name); + } else { + $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); + } + if ($parameter->default) { + $result->out->write('='); + $this->emitOne($result, $parameter->default); + } + $result->locals[$parameter->name]= true; + } + + protected function emitSignature($result, $signature) { + $result->out->write('('); + $s= sizeof($signature->parameters) - 1; + foreach ($signature->parameters as $i => $parameter) { + $this->emitParameter($result, $parameter); + if ($i < $s) $result->out->write(', '); + } + $result->out->write(')'); + + if ($signature->returns && $t= $this->returnType($signature->returns)) { + $result->out->write(':'.$t); + } + } + + protected function emitFunction($result, $function) { + $result->stack[]= $result->locals; + $result->locals= []; + + $result->out->write('function '.$function->name); + $this->emitSignature($result, $function->signature); + + $result->out->write('{'); + $this->emitAll($result, $function->body); + $result->out->write('}'); + + $result->locals= array_pop($result->stack); + } + + protected function emitClosure($result, $closure) { + $result->stack[]= $result->locals; + $result->locals= []; + + $result->out->write('function'); + $this->emitSignature($result, $closure->signature); + + if ($closure->use) { + $result->out->write(' use('.implode(',', $closure->use).') '); + foreach ($closure->use as $variable) { + $result->locals[substr($variable, 1)]= true; + } + } + $result->out->write('{'); + $this->emitAll($result, $closure->body); + $result->out->write('}'); + + $result->locals= array_pop($result->stack); + } + + protected function emitLambda($result, $lambda) { + $result->out->write('fn'); + $this->emitSignature($result, $lambda->signature); + $result->out->write('=>'); + + if (is_array($lambda->body)) { + $result->out->write('{'); + $this->emitAll($result, $lambda->body); + $result->out->write('}'); + } else { + $this->emitOne($result, $lambda->body); + } + } + + protected function emitClass($result, $class) { + array_unshift($result->meta, []); + + $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); + $class->parent && $result->out->write(' extends '.$class->parent); + $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); + $result->out->write('{'); + foreach ($class->body as $member) { + $this->emitOne($result, $member); + } + + $result->out->write('static function __init() {'); + $this->emitMeta($result, $class->name, $class->annotations, $class->comment); + $result->out->write('}} '.$class->name.'::__init();'); + } + + protected function emitAnnotations($result, $annotations) { + foreach ($annotations as $name => $annotation) { + $result->out->write("'".$name."' => "); + if ($annotation) { + $this->emitOne($result, $annotation); + $result->out->write(','); + } else { + $result->out->write('null,'); + } + } + } + + protected function emitMeta($result, $name, $annotations, $comment) { + $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); + $this->emitAnnotations($result, $annotations); + $result->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); + + foreach (array_shift($result->meta) as $type => $lookup) { + $result->out->write($type.' => ['); + foreach ($lookup as $key => $meta) { + $result->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); + $this->emitAnnotations($result, $meta[DETAIL_ANNOTATIONS]); + $result->out->write('], DETAIL_TARGET_ANNO => ['); + foreach ($meta[DETAIL_TARGET_ANNO] as $target => $annotations) { + $result->out->write("'$".$target."' => ["); + $this->emitAnnotations($result, $annotations); + $result->out->write('],'); + } + $result->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); + $result->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); + $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); + } + $result->out->write('],'); + } + $result->out->write('];'); + } + + protected function emitInterface($result, $interface) { + array_unshift($result->meta, []); + + $result->out->write('interface '.$this->declaration($interface->name)); + $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); + $result->out->write('{'); + foreach ($interface->body as $member) { + $this->emitOne($result, $member); + $result->out->write("\n"); + } + $result->out->write('}'); + + $this->emitMeta($result, $interface->name, $interface->annotations, $interface->comment); + } + + protected function emitTrait($result, $trait) { + array_unshift($result->meta, []); + + $result->out->write('trait '.$this->declaration($trait->name)); + $result->out->write('{'); + foreach ($trait->body as $member) { + $this->emitOne($result, $member); + $result->out->write("\n"); + } + + $result->out->write('static function __init() {'); + $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); + $result->out->write('}} '.$trait->name.'::__init();'); + } + + protected function emitUse($result, $use) { + $result->out->write('use '.implode(',', $use->types)); + if ($use->aliases) { + $result->out->write('{'); + foreach ($use->aliases as $reference => $alias) { + $result->out->write($reference.' as '.$alias.';'); + } + $result->out->write('}'); + } else { + $result->out->write(';'); + } + } + + protected function emitConst($result, $const) { + $result->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); + $this->emitOne($result, $const->expression); + $result->out->write(';'); + } + + protected function emitProperty($result, $property) { + $result->meta[0][self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations ? $property->annotations : [], + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); + if (isset($property->expression)) { + $result->out->write('='); + $this->emitOne($result, $property->expression); + } + $result->out->write(';'); + } + + protected function emitMethod($result, $method) { + $result->stack[]= $result->locals; + $result->locals= ['this' => true]; + $meta= [ + DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', + DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], + DETAIL_COMMENT => $method->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $declare= $promote= $params= ''; + foreach ($method->signature->parameters as $param) { + if (isset($param->promote)) { + $declare.= $param->promote.' $'.$param->name.';'; + $promote.= '$this->'.$param->name.'= $'.$param->name.';'; + $result->meta[0][self::PROPERTY][$param->name]= [ + DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', + DETAIL_ANNOTATIONS => [], + DETAIL_COMMENT => null, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + } + $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; + $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + } + $result->out->write($declare); + $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); + $this->emitSignature($result, $method->signature); + + if (null === $method->body) { + $result->out->write(';'); + } else { + $result->out->write(' {'.$promote); + $this->emitAll($result, $method->body); + $result->out->write('}'); + } + + $result->meta[0][self::METHOD][$method->name]= $meta; + $result->locals= array_pop($result->stack); + } + + protected function emitBraced($result, $braced) { + $result->out->write('('); + $this->emitOne($result, $braced->expression); + $result->out->write(')'); + } + + protected function emitBinary($result, $binary) { + $this->emitOne($result, $binary->left); + $result->out->write(' '.$binary->operator.' '); + $this->emitOne($result, $binary->right); + } + + protected function emitUnary($result, $unary) { + $result->out->write($unary->operator); + $this->emitOne($result, $unary->expression); + } + + protected function emitTernary($result, $ternary) { + $this->emitOne($result, $ternary->condition); + $result->out->write('?'); + $this->emitOne($result, $ternary->expression); + $result->out->write(':'); + $this->emitOne($result, $ternary->otherwise); + } + + protected function emitOffset($result, $offset) { + $this->emitOne($result, $offset->expression); + if (null === $offset->offset) { + $result->out->write('[]'); + } else { + $result->out->write('['); + $this->emitOne($result, $offset->offset); + $result->out->write(']'); + } + } + + protected function emitAssign($result, $target) { + if ('variable' === $target->kind) { + $result->out->write('$'.$target->name); + $result->locals[$target->name]= true; + } else if ('array' === $target->kind) { + $result->out->write('list('); + foreach ($target->values as $pair) { + $this->emitAssign($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(')'); + } else { + $this->emitOne($result, $target); + } + } + + protected function emitAssignment($result, $assignment) { + $this->emitAssign($result, $assignment->variable); + $result->out->write($assignment->operator); + $this->emitOne($result, $assignment->expression); + } + + protected function emitReturn($result, $return) { + $result->out->write('return '); + $return->expression && $this->emitOne($result, $return->expression); + } + + protected function emitIf($result, $if) { + $result->out->write('if ('); + $this->emitOne($result, $if->expression); + $result->out->write(') {'); + $this->emitAll($result, $if->body); + $result->out->write('}'); + + if ($if->otherwise) { + $result->out->write('else {'); + $this->emitAll($result, $if->otherwise); + $result->out->write('}'); + } + } + + protected function emitSwitch($result, $switch) { + $result->out->write('switch ('); + $this->emitOne($result, $switch->expression); + $result->out->write(') {'); + foreach ($switch->cases as $case) { + if ($case->expression) { + $result->out->write('case '); + $this->emitOne($result, $case->expression); + $result->out->write(':'); + } else { + $result->out->write('default:'); + } + $this->emitOne($result, $case->body); + } + $result->out->write('}'); + } + + protected function emitCatch($result, $catch) { + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + + protected function emitTry($result, $try) { + $result->out->write('try {'); + $this->emitAll($result, $try->body); + $result->out->write('}'); + if (isset($try->catches)) { + foreach ($try->catches as $catch) { + $this->emitCatch($result, $catch); + } + } + if (isset($try->finally)) { + $result->out->write('finally {'); + $this->emitAll($result, $try->finally); + $result->out->write('}'); + } + } + + protected function emitThrow($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + $result->out->write(';'); + } + + protected function emitThrowExpression($result, $throw) { + $capture= []; + foreach ($this->search($throw->expression, 'variable') as $var) { + if (isset($result->locals[$var->name])) { + $capture[$var->name]= true; + } + } + unset($capture['this']); + + $result->out->write('(function()'); + $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); + $result->out->write('{ throw '); + $this->emitOne($result, $throw->expression); + $result->out->write('; })()'); + } + + protected function emitForeach($result, $foreach) { + $result->out->write('foreach ('); + $this->emitOne($result, $foreach->expression); + $result->out->write(' as '); + if ($foreach->key) { + $this->emitOne($result, $foreach->key); + $result->out->write(' => '); + } + $this->emitOne($result, $foreach->value); + $result->out->write(') {'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); + } + + protected function emitFor($result, $for) { + $result->out->write('for ('); + $this->emitArguments($result, $for->initialization); + $result->out->write(';'); + $this->emitArguments($result, $for->condition); + $result->out->write(';'); + $this->emitArguments($result, $for->loop); + $result->out->write(') {'); + $this->emitAll($result, $for->body); + $result->out->write('}'); + } + + protected function emitDo($result, $do) { + $result->out->write('do'); + $result->out->write('{'); + $this->emitAll($result, $do->body); + $result->out->write('} while ('); + $this->emitOne($result, $do->expression); + $result->out->write(');'); + } + + protected function emitWhile($result, $while) { + $result->out->write('while ('); + $this->emitOne($result, $while->expression); + $result->out->write(') {'); + $this->emitAll($result, $while->body); + $result->out->write('}'); + } + + protected function emitBreak($result, $break) { + $result->out->write('break '); + $break->expression && $this->emitOne($result, $break->expression); + $result->out->write(';'); + } + + protected function emitContinue($result, $continue) { + $result->out->write('continue '); + $continue->expression && $this->emitOne($result, $continue->expression); + $result->out->write(';'); + } + + protected function emitLabel($result, $label) { + $result->out->write($label->name.':'); + } + + protected function emitGoto($result, $goto) { + $result->out->write('goto '.$goto->label); + } + + protected function emitInstanceOf($result, $instanceof) { + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '); + if ($instanceof->type instanceof Element) { + $this->emitOne($result, $instanceof->type); + } else { + $result->out->write($instanceof->type); + } + } + + protected function emitArguments($result, $arguments) { + $s= sizeof($arguments) - 1; + foreach ($arguments as $i => $argument) { + $this->emitOne($result, $argument); + if ($i < $s) $result->out->write(', '); + } + } + + protected function emitNew($result, $new) { + $result->out->write('new '.$new->type.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + + protected function emitNewClass($result, $new) { + $result->out->write('new class('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + + $new->definition->parent && $result->out->write(' extends '.$new->definition->parent); + $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); + $result->out->write('{'); + foreach ($new->definition->body as $member) { + $this->emitOne($result, $member); + $result->out->write("\n"); + } + $result->out->write('}'); + } + + protected function emitInvoke($result, $invoke) { + $this->emitOne($result, $invoke->expression); + $result->out->write('('); + $this->emitArguments($result, $invoke->arguments); + $result->out->write(')'); + } + + protected function emitScope($result, $scope) { + $result->out->write($scope->type.'::'); + $this->emitOne($result, $scope->member); + } + + protected function emitInstance($result, $instance) { + if ('new' === $instance->expression->kind) { + $result->out->write('('); + $this->emitOne($result, $instance->expression); + $result->out->write(')->'); + } else { + $this->emitOne($result, $instance->expression); + $result->out->write('->'); + } + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + + protected function emitUnpack($result, $unpack) { + $result->out->write('...'); + $this->emitOne($result, $unpack->expression); + } + + protected function emitYield($result, $yield) { + $result->out->write('yield '); + if ($yield->key) { + $this->emitOne($result, $yield->key); + $result->out->write('=>'); + } + if ($yield->value) { + $this->emitOne($result, $yield->value); + } + } + + protected function emitFrom($result, $from) { + $result->out->write('yield from '); + $this->emitOne($result, $from->iterable); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index eab53629..26571a2d 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -1,14 +1,11 @@ Date: Mon, 9 Sep 2019 13:38:23 +0200 Subject: [PATCH 109/926] Extend PHP base class --- src/main/php/lang/ast/emit/HHVM320.class.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/HHVM320.class.php b/src/main/php/lang/ast/emit/HHVM320.class.php index 3491c3d1..fa4167e2 100755 --- a/src/main/php/lang/ast/emit/HHVM320.class.php +++ b/src/main/php/lang/ast/emit/HHVM320.class.php @@ -1,12 +1,10 @@ Date: Mon, 9 Sep 2019 13:39:25 +0200 Subject: [PATCH 110/926] QA: Make emit() protected, rename argument for clarity --- src/main/php/lang/ast/Emitter.class.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 6a1c315c..2336e6aa 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -72,13 +72,14 @@ public function transformations() { /** * Search a given scope recursively for nodes with a given kind * - * @param lang.ast.Element $arg + * @param lang.ast.Element $node * @param string $kind * @return iterable */ - protected function search($arg, $kind) { - if ($arg->kind === $kind) yield $arg; - foreach ($arg->children() as $child) { + protected function search($node, $kind) { + if ($node->kind === $kind) yield $node; + + foreach ($node->children() as $child) { foreach ($this->search($child, $kind) as $result) { yield $result; } @@ -90,7 +91,7 @@ protected function search($arg, $kind) { * * @return void */ - public function emit() { + protected function emit() { throw new IllegalStateException('Called without node kind'); } From e2a32fb9c78cd946e520b5831465b705ab90895b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 14:37:58 +0200 Subject: [PATCH 111/926] Rename Node -> Token --- src/main/php/lang/ast/Parse.class.php | 10 +++++----- .../php/lang/ast/{Node.class.php => Token.class.php} | 2 +- src/main/php/lang/ast/syntax/PHP.class.php | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/main/php/lang/ast/{Node.class.php => Token.class.php} (96%) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 84779fb3..2ab166d2 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -65,16 +65,16 @@ public function forward() { list($value, $line)= $this->tokens->current(); $this->tokens->next(); if ('name' === $type) { - $node= new Node(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); + $node= new Token(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); $node->kind= $type; } else if ('operator' === $type) { - $node= new Node($this->language->symbol($value)); + $node= new Token($this->language->symbol($value)); $node->kind= $type; } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $node= new Node($this->language->symbol('(literal)')); + $node= new Token($this->language->symbol('(literal)')); $node->kind= 'literal'; } else if ('variable' === $type) { - $node= new Node($this->language->symbol('(variable)')); + $node= new Token($this->language->symbol('(variable)')); $node->kind= 'variable'; } else if ('comment' === $type) { $this->comment= $value; @@ -89,7 +89,7 @@ public function forward() { return; } - $node= new Node($this->language->symbol('(end)')); + $node= new Token($this->language->symbol('(end)')); $node->line= $line; $this->token= $node; } diff --git a/src/main/php/lang/ast/Node.class.php b/src/main/php/lang/ast/Token.class.php similarity index 96% rename from src/main/php/lang/ast/Node.class.php rename to src/main/php/lang/ast/Token.class.php index 2d9506a9..223b4a25 100755 --- a/src/main/php/lang/ast/Node.class.php +++ b/src/main/php/lang/ast/Token.class.php @@ -3,7 +3,7 @@ use lang\Value; use util\Objects; -class Node implements Value { +class Token implements Value { public $symbol, $value, $kind; public $comment= null; public $line= -1; diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 7b9efa9f..52632f9d 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -4,7 +4,7 @@ use lang\ast\FunctionType; use lang\ast\Language; use lang\ast\MapType; -use lang\ast\Node; +use lang\ast\Token; use lang\ast\Type; use lang\ast\UnionType; use lang\ast\nodes\Annotations; @@ -400,7 +400,7 @@ public function __construct() { } $parse->queue= [$parse->token]; - $parse->token= new Node($this->symbol(';')); + $parse->token= new Token($this->symbol(';')); return new FunctionDeclaration($name, $signature, $statements, $node->line); } }); @@ -436,7 +436,7 @@ public function __construct() { $this->prefix('(name)', function($parse, $node) { if (':' === $parse->token->value) { - $parse->token= new Node($this->symbol(';')); + $parse->token= new Token($this->symbol(';')); return new Label($node->value, $node->line); } else { return new Literal($node->value, $node->line); @@ -976,7 +976,7 @@ private function type0($parse, $optional) { } else if ('>' === $parse->token->symbol->id) { break; } else if ('>>' === $parse->token->value) { - $parse->queue[]= $parse->token= new Node(self::symbol('>')); + $parse->queue[]= $parse->token= new Token(self::symbol('>')); break; } } while (true); From 87dc9c52223ef650b2abe163e5f5863090f59d0a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:06:46 +0200 Subject: [PATCH 112/926] Rename local $node -> $t(oken) --- src/main/php/lang/ast/Parse.class.php | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index 2ab166d2..e6205b83 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -65,17 +65,17 @@ public function forward() { list($value, $line)= $this->tokens->current(); $this->tokens->next(); if ('name' === $type) { - $node= new Token(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); - $node->kind= $type; + $t= new Token(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); + $t->kind= $type; } else if ('operator' === $type) { - $node= new Token($this->language->symbol($value)); - $node->kind= $type; + $t= new Token($this->language->symbol($value)); + $t->kind= $type; } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $node= new Token($this->language->symbol('(literal)')); - $node->kind= 'literal'; + $t= new Token($this->language->symbol('(literal)')); + $t->kind= 'literal'; } else if ('variable' === $type) { - $node= new Token($this->language->symbol('(variable)')); - $node->kind= 'variable'; + $t= new Token($this->language->symbol('(variable)')); + $t->kind= 'variable'; } else if ('comment' === $type) { $this->comment= $value; continue; @@ -83,15 +83,15 @@ public function forward() { throw new Error('Unexpected token '.$value, $this->file, $line); } - $node->value= $value; - $node->line= $line; - $this->token= $node; + $t->value= $value; + $t->line= $line; + $this->token= $t; return; } - $node= new Token($this->language->symbol('(end)')); - $node->line= $line; - $this->token= $node; + $t= new Token($this->language->symbol('(end)')); + $t->line= $line; + $this->token= $t; } /** From 21b893dd92323a6095d2e4b0501472be31b4fc49 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:25:46 +0200 Subject: [PATCH 113/926] Rename parameters / locals $node -> $token --- src/main/php/lang/ast/Language.class.php | 28 ++-- src/main/php/lang/ast/syntax/PHP.class.php | 186 ++++++++++----------- 2 files changed, 107 insertions(+), 107 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index da34135e..1f3e4858 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -33,50 +33,50 @@ public function symbol($id, $lbp= 0) { public function constant($id, $value) { $const= $this->symbol($id); - $const->nud= function($parse, $node) use($value) { - return new Literal($value, $node->line); + $const->nud= function($parse, $token) use($value) { + return new Literal($value, $token->line); }; } public function assignment($id) { $infix= $this->symbol($id, 10); - $infix->led= function($parse, $node, $left) use($id) { - return new Assignment($left, $id, $this->expression($parse, 9), $node->line); + $infix->led= function($parse, $token, $left) use($id) { + return new Assignment($left, $id, $this->expression($parse, 9), $token->line); }; } public function infix($id, $bp, $led= null) { $infix= $this->symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expression($parse, $bp), $node->line); + $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id, $bp) { + return new BinaryExpression($left, $id, $this->expression($parse, $bp), $token->line); }; } public function infixr($id, $bp, $led= null) { $infix= $this->symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $node->line); + $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id, $bp) { + return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $token->line); }; } public function infixt($id, $bp) { $infix= $this->symbol($id, $bp); - $infix->led= function($parse, $node, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1), $node->line); + $infix->led= function($parse, $token, $left) use($id, $bp) { + return new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1), $token->line); }; } public function prefix($id, $nud= null) { $prefix= $this->symbol($id); - $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $node) use($id) { - return new UnaryExpression($this->expression($parse, 0), $id, $node->line); + $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id) { + return new UnaryExpression($this->expression($parse, 0), $id, $token->line); }; } public function suffix($id, $bp, $led= null) { $suffix= $this->symbol($id, $bp); - $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $node, $left) use($id) { - return new UnaryExpression($left, $id, $node->line); + $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id) { + return new UnaryExpression($left, $id, $token->line); }; } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 52632f9d..0e084efe 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -119,30 +119,30 @@ public function __construct() { $this->infixr('<<', 70); $this->infixr('>>', 70); - $this->infix('instanceof', 60, function($parse, $node, $left) { + $this->infix('instanceof', 60, function($parse, $token, $left) { if ('name' === $parse->token->kind) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); - return new InstanceOfExpression($left, $type, $node->line); + return new InstanceOfExpression($left, $type, $token->line); } else { - return new InstanceOfExpression($left, $this->expression($parse, 0), $node->line); + return new InstanceOfExpression($left, $this->expression($parse, 0), $token->line); } }); - $this->infix('->', 80, function($parse, $node, $left) { + $this->infix('->', 80, function($parse, $token, $left) { if ('{' === $parse->token->value) { $parse->forward(); $expr= $this->expression($parse, 0); $parse->expecting('}', 'dynamic member'); } else { - $expr= new Literal($parse->token->value, $node->line); + $expr= new Literal($parse->token->value, $token->line); $parse->forward(); } - return new InstanceExpression($left, $expr, $node->line); + return new InstanceExpression($left, $expr, $token->line); }); - $this->infix('::', 80, function($parse, $node, $left) { + $this->infix('::', 80, function($parse, $token, $left) { $scope= $parse->scope->resolve($left->expression); if ('variable' === $parse->token->kind) { @@ -155,16 +155,16 @@ public function __construct() { } $parse->forward(); - return new ScopeExpression($scope, $expr, $node->line); + return new ScopeExpression($scope, $expr, $token->line); }); - $this->infix('(', 80, function($parse, $node, $left) { + $this->infix('(', 80, function($parse, $token, $left) { $arguments= $this->expressions($parse); $parse->expecting(')', 'invoke expression'); - return new InvokeExpression($left, $arguments, $node->line); + return new InvokeExpression($left, $arguments, $token->line); }); - $this->infix('[', 80, function($parse, $node, $left) { + $this->infix('[', 80, function($parse, $token, $left) { if (']' === $parse->token->value) { $expr= null; $parse->forward(); @@ -173,20 +173,20 @@ public function __construct() { $parse->expecting(']', 'offset access'); } - return new OffsetExpression($left, $expr, $node->line); + return new OffsetExpression($left, $expr, $token->line); }); - $this->infix('{', 80, function($parse, $node, $left) { + $this->infix('{', 80, function($parse, $token, $left) { $expr= $this->expression($parse, 0); $parse->expecting('}', 'dynamic member'); - return new OffsetExpression($left, $expr, $node->line); + return new OffsetExpression($left, $expr, $token->line); }); - $this->infix('?', 80, function($parse, $node, $left) { + $this->infix('?', 80, function($parse, $token, $left) { $when= $this->expressionWithThrows($parse, 0); $parse->expecting(':', 'ternary'); $else= $this->expressionWithThrows($parse, 0); - return new TernaryExpression($left, $when, $else, $node->line); + return new TernaryExpression($left, $when, $else, $token->line); }); $this->prefix('@'); @@ -220,7 +220,7 @@ public function __construct() { // - A cast `(int)$a` or `(int)($a / 2)`. // // Resolve by looking ahead after the closing ")" - $this->prefix('(', function($parse, $node) { + $this->prefix('(', function($parse, $token) { static $types= [ '<' => true, '>' => true, @@ -229,7 +229,7 @@ public function __construct() { ':' => true ]; - $skipped= [$node, $parse->token]; + $skipped= [$token, $parse->token]; $cast= true; $level= 1; while ($level > 0 && null !== $parse->token->value) { @@ -259,23 +259,23 @@ public function __construct() { $statements= $this->expressionWithThrows($parse, 0); } - return new LambdaExpression($signature, $statements, $node->line); + return new LambdaExpression($signature, $statements, $token->line); } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { $parse->forward(); $parse->expecting('(', 'cast'); $type= $this->type0($parse, false); $parse->expecting(')', 'cast'); - return new CastExpression($type, $this->expression($parse, 0), $node->line); + return new CastExpression($type, $this->expression($parse, 0), $token->line); } else { $parse->forward(); $parse->expecting('(', 'braced'); $expr= $this->expression($parse, 0); $parse->expecting(')', 'braced'); - return new Braced($expr, $node->line); + return new Braced($expr, $token->line); } }); - $this->prefix('[', function($parse, $node) { + $this->prefix('[', function($parse, $token) { $values= []; while (']' !== $parse->token->value) { $expr= $this->expression($parse, 0); @@ -295,10 +295,10 @@ public function __construct() { } $parse->expecting(']', 'array literal'); - return new ArrayLiteral($values, $node->line); + return new ArrayLiteral($values, $token->line); }); - $this->prefix('new', function($parse, $node) { + $this->prefix('new', function($parse, $token) { $type= $parse->token; $parse->forward(); @@ -307,36 +307,36 @@ public function __construct() { $parse->expecting(')', 'new arguments'); if ('variable' === $type->kind) { - return new NewExpression('$'.$type->value, $arguments, $node->line); + return new NewExpression('$'.$type->value, $arguments, $token->line); } else if ('class' === $type->value) { - return new NewClassExpression($this->clazz($parse, null), $arguments, $node->line); + return new NewClassExpression($this->clazz($parse, null), $arguments, $token->line); } else { - return new NewExpression($parse->scope->resolve($type->value), $arguments, $node->line); + return new NewExpression($parse->scope->resolve($type->value), $arguments, $token->line); } }); - $this->prefix('yield', function($parse, $node) { + $this->prefix('yield', function($parse, $token) { if (';' === $parse->token->value) { - return new YieldExpression(null, null, $node->line); + return new YieldExpression(null, null, $token->line); } else if ('from' === $parse->token->value) { $parse->forward(); - return new YieldFromExpression($this->expression($parse, 0), $node->line); + return new YieldFromExpression($this->expression($parse, 0), $token->line); } else { $expr= $this->expression($parse, 0); if ('=>' === $parse->token->value) { $parse->forward(); - return new YieldExpression($expr, $this->expression($parse, 0), $node->line); + return new YieldExpression($expr, $this->expression($parse, 0), $token->line); } else { - return new YieldExpression(null, $expr, $node->line); + return new YieldExpression(null, $expr, $token->line); } } }); - $this->prefix('...', function($parse, $node) { - return new UnpackExpression($this->expression($parse, 0), $node->line); + $this->prefix('...', function($parse, $token) { + return new UnpackExpression($this->expression($parse, 0), $token->line); }); - $this->prefix('fn', function($parse, $node) { + $this->prefix('fn', function($parse, $token) { $signature= $this->signature($parse); $parse->expecting('=>', 'fn'); @@ -348,10 +348,10 @@ public function __construct() { $statements= $this->expressionWithThrows($parse, 0); } - return new LambdaExpression($signature, $statements, $node->line); + return new LambdaExpression($signature, $statements, $token->line); }); - $this->prefix('function', function($parse, $node) { + $this->prefix('function', function($parse, $token) { // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; // the latter explicitely becomes a statement by pushing a semicolon. @@ -382,7 +382,7 @@ public function __construct() { $statements= $this->statements($parse); $parse->expecting('}', 'function'); - return new ClosureExpression($signature, $use, $statements, $node->line); + return new ClosureExpression($signature, $use, $statements, $token->line); } else { $name= $parse->token->value; $parse->forward(); @@ -391,7 +391,7 @@ public function __construct() { if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' $parse->forward(); $expr= $this->expressionWithThrows($parse, 0); - $statements= [new ReturnStatement($expr, $node->line)]; + $statements= [new ReturnStatement($expr, $token->line)]; $parse->expecting(';', 'function'); } else { // Regular function $parse->expecting('{', 'function'); @@ -401,11 +401,11 @@ public function __construct() { $parse->queue= [$parse->token]; $parse->token= new Token($this->symbol(';')); - return new FunctionDeclaration($name, $signature, $statements, $node->line); + return new FunctionDeclaration($name, $signature, $statements, $token->line); } }); - $this->prefix('static', function($parse, $node) { + $this->prefix('static', function($parse, $token) { if ('variable' === $parse->token->kind) { $init= []; while (';' !== $parse->token->value) { @@ -423,60 +423,60 @@ public function __construct() { $parse->forward(); } } - return new StaticLocals($init, $node->line); + return new StaticLocals($init, $token->line); } - return new Literal($node->value, $node->line); + return new Literal($token->value, $token->line); }); - $this->prefix('goto', function($parse, $node) { + $this->prefix('goto', function($parse, $token) { $label= $parse->token->value; $parse->forward(); - return new GotoStatement($label, $node->line); + return new GotoStatement($label, $token->line); }); - $this->prefix('(name)', function($parse, $node) { + $this->prefix('(name)', function($parse, $token) { if (':' === $parse->token->value) { $parse->token= new Token($this->symbol(';')); - return new Label($node->value, $node->line); + return new Label($token->value, $token->line); } else { - return new Literal($node->value, $node->line); + return new Literal($token->value, $token->line); } }); - $this->prefix('(variable)', function($parse, $node) { - return new Variable($node->value, $node->line); + $this->prefix('(variable)', function($parse, $token) { + return new Variable($token->value, $token->line); }); - $this->prefix('(literal)', function($parse, $node) { - return new Literal($node->value, $node->line); + $this->prefix('(literal)', function($parse, $token) { + return new Literal($token->value, $token->line); }); - $this->stmt('stmt('token->value; $parse->forward(); - return new Start($syntax, $node->line); + return new Start($syntax, $token->line); }); - $this->stmt('{', function($parse, $node) { + $this->stmt('{', function($parse, $token) { $statements= $this->statements($parse); $parse->expecting('}', 'block'); - return new Block($statements, $node->line); + return new Block($statements, $token->line); }); - $this->prefix('echo', function($parse, $node) { - return new EchoStatement($this->expressions($parse, ';'), $node->line); + $this->prefix('echo', function($parse, $token) { + return new EchoStatement($this->expressions($parse, ';'), $token->line); }); - $this->stmt('namespace', function($parse, $node) { + $this->stmt('namespace', function($parse, $token) { $name= $parse->token->value; $parse->forward(); $parse->expecting(';', 'namespace'); $parse->scope->package($name); - return new NamespaceDeclaration($name, $node->line); + return new NamespaceDeclaration($name, $token->line); }); - $this->stmt('use', function($parse, $node) { + $this->stmt('use', function($parse, $token) { if ('function' === $parse->token->value) { $type= 'function'; $parse->forward(); @@ -528,10 +528,10 @@ public function __construct() { } $parse->expecting(';', 'use'); - return new UseStatement($type, $names, $node->line); + return new UseStatement($type, $names, $token->line); }); - $this->stmt('if', function($parse, $node) { + $this->stmt('if', function($parse, $token) { $parse->expecting('(', 'if'); $condition= $this->expression($parse, 0); $parse->expecting(')', 'if'); @@ -543,10 +543,10 @@ public function __construct() { $otherwise= null; } - return new IfStatement($condition, $when, $otherwise, $node->line); + return new IfStatement($condition, $when, $otherwise, $token->line); }); - $this->stmt('switch', function($parse, $node) { + $this->stmt('switch', function($parse, $token) { $parse->expecting('(', 'switch'); $condition= $this->expression($parse, 0); $parse->expecting(')', 'switch'); @@ -569,10 +569,10 @@ public function __construct() { } $parse->forward(); - return new SwitchStatement($condition, $cases, $node->line); + return new SwitchStatement($condition, $cases, $token->line); }); - $this->stmt('break', function($parse, $node) { + $this->stmt('break', function($parse, $token) { if (';' === $parse->token->value) { $expr= null; $parse->forward(); @@ -581,10 +581,10 @@ public function __construct() { $parse->expecting(';', 'break'); } - return new BreakStatement($expr, $node->line); + return new BreakStatement($expr, $token->line); }); - $this->stmt('continue', function($parse, $node) { + $this->stmt('continue', function($parse, $token) { if (';' === $parse->token->value) { $expr= null; $parse->forward(); @@ -593,28 +593,28 @@ public function __construct() { $parse->expecting(';', 'continue'); } - return new ContinueStatement($expr, $node->line); + return new ContinueStatement($expr, $token->line); }); - $this->stmt('do', function($parse, $node) { + $this->stmt('do', function($parse, $token) { $block= $this->block($parse); $parse->expecting('while', 'do'); $parse->expecting('(', 'do'); $expression= $this->expression($parse, 0); $parse->expecting(')', 'do'); $parse->expecting(';', 'do'); - return new DoLoop($expression, $block, $node->line); + return new DoLoop($expression, $block, $token->line); }); - $this->stmt('while', function($parse, $node) { + $this->stmt('while', function($parse, $token) { $parse->expecting('(', 'while'); $expression= $this->expression($parse, 0); $parse->expecting(')', 'while'); $block= $this->block($parse); - return new WhileLoop($expression, $block, $node->line); + return new WhileLoop($expression, $block, $token->line); }); - $this->stmt('for', function($parse, $node) { + $this->stmt('for', function($parse, $token) { $parse->expecting('(', 'for'); $init= $this->expressions($parse, ';'); $parse->expecting(';', 'for'); @@ -623,10 +623,10 @@ public function __construct() { $loop= $this->expressions($parse, ')'); $parse->expecting(')', 'for'); $block= $this->block($parse); - return new ForLoop($init, $cond, $loop, $block, $node->line); + return new ForLoop($init, $cond, $loop, $block, $token->line); }); - $this->stmt('foreach', function($parse, $node) { + $this->stmt('foreach', function($parse, $token) { $parse->expecting('(', 'foreach'); $expression= $this->expression($parse, 0); $parse->expecting('as', 'foreach'); @@ -643,16 +643,16 @@ public function __construct() { $parse->expecting(')', 'foreach'); $block= $this->block($parse); - return new ForeachLoop($expression, $key, $value, $block, $node->line); + return new ForeachLoop($expression, $key, $value, $block, $token->line); }); - $this->stmt('throw', function($parse, $node) { + $this->stmt('throw', function($parse, $token) { $expr= $this->expression($parse, 0); $parse->expecting(';', 'throw'); - return new ThrowStatement($expr, $node->line); + return new ThrowStatement($expr, $token->line); }); - $this->stmt('try', function($parse, $node) { + $this->stmt('try', function($parse, $token) { $parse->expecting('{', 'try'); $statements= $this->statements($parse); $parse->expecting('}', 'try'); @@ -688,10 +688,10 @@ public function __construct() { $finally= null; } - return new TryStatement($statements, $catches, $finally, $node->line); + return new TryStatement($statements, $catches, $finally, $token->line); }); - $this->stmt('return', function($parse, $node) { + $this->stmt('return', function($parse, $token) { if (';' === $parse->token->value) { $expr= null; $parse->forward(); @@ -700,24 +700,24 @@ public function __construct() { $parse->expecting(';', 'return'); } - return new ReturnStatement($expr, $node->line); + return new ReturnStatement($expr, $token->line); }); - $this->stmt('abstract', function($parse, $node) { + $this->stmt('abstract', function($parse, $token) { $parse->forward(); $type= $parse->scope->resolve($parse->token->value); $parse->forward(); return $this->clazz($parse, $type, ['abstract']); }); - $this->stmt('final', function($parse, $node) { + $this->stmt('final', function($parse, $token) { $parse->forward(); $type= $parse->scope->resolve($parse->token->value); $parse->forward(); return $this->clazz($parse, $type, ['final']); }); - $this->stmt('<<', function($parse, $node) { + $this->stmt('<<', function($parse, $token) { $values= []; do { $name= $parse->token->value; @@ -743,17 +743,17 @@ public function __construct() { } while (null !== $parse->token->value); $parse->forward(); - return new Annotations($values, $node->line); + return new Annotations($values, $token->line); }); - $this->stmt('class', function($parse, $node) { + $this->stmt('class', function($parse, $token) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); return $this->clazz($parse, $type); }); - $this->stmt('interface', function($parse, $node) { + $this->stmt('interface', function($parse, $token) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); $comment= $parse->comment; @@ -779,12 +779,12 @@ public function __construct() { $body= $this->typeBody($parse); $parse->expecting('}', 'interface'); - $decl= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment, $node->line); + $decl= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment, $token->line); $parse->scope->annotations= []; return $decl; }); - $this->stmt('trait', function($parse, $node) { + $this->stmt('trait', function($parse, $token) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); $comment= $parse->comment; @@ -794,7 +794,7 @@ public function __construct() { $body= $this->typeBody($parse); $parse->expecting('}', 'trait'); - $decl= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment, $node->line); + $decl= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment, $token->line); $parse->scope->annotations= []; return $decl; }); From b4a17f6c26740506808bf87124da6599ebb4fce9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:44:53 +0200 Subject: [PATCH 114/926] Adapt to Element -> Node renaming See xp-framework/ast#4 --- src/main/php/lang/ast/Emitter.class.php | 8 ++++---- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- src/test/php/lang/ast/unittest/EmitterTest.class.php | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 2336e6aa..4e01ad2d 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -2,7 +2,7 @@ use lang\IllegalArgumentException; use lang\IllegalStateException; -use lang\ast\nodes\Value; +use lang\ast\Node; use lang\reflect\Package; abstract class Emitter { @@ -113,7 +113,7 @@ public function emitAll($result, $nodes) { * Emit single nodes * * @param lang.ast.Result $result - * @param lang.ast.Element $node + * @param lang.ast.Node $node * @return void */ public function emitOne($result, $node) { @@ -125,8 +125,8 @@ public function emitOne($result, $node) { // Check for transformations if (isset($this->transformations[$node->kind])) { foreach ($this->transformations[$node->kind] as $transformation) { - $r= $transformation($node); - if ($r instanceof Value) { + $r= $transformation($result->codegen, $node); + if ($r instanceof Node) { if ($r->kind === $node->kind) continue; $this->{'emit'.$r->kind}($result, $r); return; diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 47a7040e..8d07b3d0 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ emitOne($result, $instanceof->expression); $result->out->write(' instanceof '); - if ($instanceof->type instanceof Element) { + if ($instanceof->type instanceof Node) { $this->emitOne($result, $instanceof->type); } else { $result->out->write($instanceof->type); diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index d3d12e93..630ffc51 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -2,8 +2,8 @@ use io\streams\MemoryOutputStream; use lang\IllegalStateException; -use lang\ast\Element; use lang\ast\Emitter; +use lang\ast\Node; use lang\ast\Result; use unittest\TestCase; @@ -55,9 +55,8 @@ public function remove_unsets_empty_kind() { } #[@test, @expect(IllegalStateException::class)] - public function emit_element_without_kind() { - $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), newinstance(Element::class, [], [ - 'line' => -1, + public function emit_node_without_kind() { + $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), newinstance(Node::class, [], [ 'kind' => null ])); } From 8e9b1c81272a22c7c3b2d2d6e2351ebb00c6e15f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:45:15 +0200 Subject: [PATCH 115/926] Use xp-framework/ast master as 3.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6af85669..281d1c06 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "^2.0", + "xp-framework/ast": "dev-master as 3.0.0", "php" : ">=5.6.0" }, "require-dev" : { From fd47337eede4c23fa3c358b7fa0dcca1733d73ee Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:50:54 +0200 Subject: [PATCH 116/926] Add CodeGen class, pass this to transformation API --- src/main/php/lang/ast/CodeGen.class.php | 25 +++++++++++++++++++ src/main/php/lang/ast/Emitter.class.php | 17 ------------- src/main/php/lang/ast/Result.class.php | 6 ++--- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/main/php/lang/ast/emit/PHP56.class.php | 2 +- .../emit/RewriteLambdaExpressions.class.php | 2 +- .../lang/ast/syntax/php/NullSafe.class.php | 2 +- .../php/lang/ast/syntax/php/Using.class.php | 2 +- .../emit/TransformationsTest.class.php | 4 +-- 9 files changed, 35 insertions(+), 27 deletions(-) create mode 100755 src/main/php/lang/ast/CodeGen.class.php diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php new file mode 100755 index 00000000..755e85f9 --- /dev/null +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -0,0 +1,25 @@ +id++); } + + /** + * Search a given scope recursively for nodes with a given kind + * + * @param lang.ast.Node $node + * @param string $kind + * @return iterable + */ + public function search($node, $kind) { + if ($node->kind === $kind) yield $node; + + foreach ($node->children() as $child) { + foreach ($this->search($child, $kind) as $result) { + yield $result; + } + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 4e01ad2d..031146d3 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -69,23 +69,6 @@ public function transformations() { return $this->transformations; } - /** - * Search a given scope recursively for nodes with a given kind - * - * @param lang.ast.Element $node - * @param string $kind - * @return iterable - */ - protected function search($node, $kind) { - if ($node->kind === $kind) yield $node; - - foreach ($node->children() as $child) { - foreach ($this->search($child, $kind) as $result) { - yield $result; - } - } - } - /** * Catch-all, should `$node->kind` be empty in `{'emit'.$node->kind}`. * diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 242ad968..9deae5b1 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -4,9 +4,8 @@ use io\streams\StringWriter; class Result { - private $id= 0; - public $out; + public $codegen; public $line= 1; public $meta= []; public $locals= []; @@ -16,6 +15,7 @@ class Result { /** @param io.streams.Writer */ public function __construct($out) { $this->out= $out; + $this->codegen= new CodeGen(); } /** @@ -24,7 +24,7 @@ public function __construct($out) { * @return string */ public function temp() { - return '$T'.($this->id++); + return '$'.$this->codegen->symbol(); } /** diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8d07b3d0..988906c3 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -554,7 +554,7 @@ protected function emitThrow($result, $throw) { protected function emitThrowExpression($result, $throw) { $capture= []; - foreach ($this->search($throw->expression, 'variable') as $var) { + foreach ($result->codegen->search($throw->expression, 'variable') as $var) { if (isset($result->locals[$var->name])) { $capture[$var->name]= true; } diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 26571a2d..4e81c15e 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -179,7 +179,7 @@ protected function emitInvoke($result, $invoke) { /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ protected function emitThrowExpression($result, $throw) { $capture= []; - foreach ($this->search($throw, 'variable') as $var) { + foreach ($result->codegen->search($throw, 'variable') as $var) { if (isset($result->locals[$var->name])) { $capture[$var->name]= true; } diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index ddb357cf..ca691c1e 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -9,7 +9,7 @@ trait RewriteLambdaExpressions { protected function emitLambda($result, $lambda) { $capture= []; - foreach ($this->search($lambda, 'variable') as $var) { + foreach ($result->codegen->search($lambda, 'variable') as $var) { if (isset($result->locals[$var->name])) { $capture[$var->name]= true; } diff --git a/src/main/php/lang/ast/syntax/php/NullSafe.class.php b/src/main/php/lang/ast/syntax/php/NullSafe.class.php index 347d9067..e6f189e5 100755 --- a/src/main/php/lang/ast/syntax/php/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/php/NullSafe.class.php @@ -41,7 +41,7 @@ public function setup($language, $emitter) { return $value; }); - $emitter->transform('nullsafeinstance', function($node) { + $emitter->transform('nullsafeinstance', function($codegen, $node) { static $i= 0; $temp= new Variable('_N'.($i++)); diff --git a/src/main/php/lang/ast/syntax/php/Using.class.php b/src/main/php/lang/ast/syntax/php/Using.class.php index a666fae5..21d995be 100755 --- a/src/main/php/lang/ast/syntax/php/Using.class.php +++ b/src/main/php/lang/ast/syntax/php/Using.class.php @@ -49,7 +49,7 @@ public function setup($language, $emitter) { return new UsingStatement($arguments, $statements); }); - $emitter->transform('using', function($node) { + $emitter->transform('using', function($codegen, $node) { static $i= 0; $cleanup= []; diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 0f3dafe8..d5db239b 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -9,7 +9,7 @@ class TransformationsTest extends EmittingTest { /** @return void */ public function setUp() { - $this->transform('class', function($class) { + $this->transform('class', function($codegen, $class) { if ($class->annotation('repr')) { $class->inject(new Method( ['public'], @@ -20,7 +20,7 @@ public function setUp() { } return $class; }); - $this->transform('class', function($class) { + $this->transform('class', function($codegen, $class) { if ($class->annotation('getters')) { foreach ($class->properties() as $property) { $class->inject(new Method( From 5179eff951b6fae8b4e2c4d16c946071df158fcc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 15:53:13 +0200 Subject: [PATCH 117/926] Use CodeGen::symbol() instead of creating temporary variables ourselves --- src/main/php/lang/ast/syntax/php/NullSafe.class.php | 4 +--- src/main/php/lang/ast/syntax/php/Using.class.php | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/syntax/php/NullSafe.class.php b/src/main/php/lang/ast/syntax/php/NullSafe.class.php index e6f189e5..2f96de1d 100755 --- a/src/main/php/lang/ast/syntax/php/NullSafe.class.php +++ b/src/main/php/lang/ast/syntax/php/NullSafe.class.php @@ -42,9 +42,7 @@ public function setup($language, $emitter) { }); $emitter->transform('nullsafeinstance', function($codegen, $node) { - static $i= 0; - - $temp= new Variable('_N'.($i++)); + $temp= new Variable($codegen->symbol()); $null= new Literal('null'); return new TernaryExpression( new BinaryExpression($null, '===', new Braced(new Assignment($temp, '=', $node->expression))), diff --git a/src/main/php/lang/ast/syntax/php/Using.class.php b/src/main/php/lang/ast/syntax/php/Using.class.php index 21d995be..9741a5ad 100755 --- a/src/main/php/lang/ast/syntax/php/Using.class.php +++ b/src/main/php/lang/ast/syntax/php/Using.class.php @@ -50,14 +50,12 @@ public function setup($language, $emitter) { }); $emitter->transform('using', function($codegen, $node) { - static $i= 0; - $cleanup= []; foreach ($node->arguments as $expression) { switch ($expression->kind) { case 'variable': $variable= $expression; yield $expression; break; case 'assignment': $variable= $expression->variable; yield $expression; break; - default: $variable= new Variable('_U'.($i++)); yield new Assignment($variable, '=', $expression); break; + default: $variable= new Variable($codegen->symbol()); yield new Assignment($variable, '=', $expression); break; } $cleanup[]= new IfStatement(new InstanceOfExpression($variable, '\lang\Closeable'), From cfc9453a0c0833111933a29a6b6413f1abf9599b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 16:06:40 +0200 Subject: [PATCH 118/926] Extract throw expressions --- src/main/php/lang/ast/Language.class.php | 2 +- src/main/php/lang/ast/syntax/PHP.class.php | 22 +++++-------------- .../ast/syntax/php/CompactMethods.class.php | 2 +- .../syntax/php/HackArrowFunctions.class.php | 2 +- .../ast/syntax/php/ThrowExpressions.class.php | 13 +++++++++++ 5 files changed, 22 insertions(+), 19 deletions(-) create mode 100755 src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 1f3e4858..454709bb 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -62,7 +62,7 @@ public function infixr($id, $bp, $led= null) { public function infixt($id, $bp) { $infix= $this->symbol($id, $bp); $infix->led= function($parse, $token, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expressionWithThrows($parse, $bp - 1), $token->line); + return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $token->line); }; } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 0e084efe..00a14bf5 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -183,9 +183,9 @@ public function __construct() { }); $this->infix('?', 80, function($parse, $token, $left) { - $when= $this->expressionWithThrows($parse, 0); + $when= $this->expression($parse, 0); $parse->expecting(':', 'ternary'); - $else= $this->expressionWithThrows($parse, 0); + $else= $this->expression($parse, 0); return new TernaryExpression($left, $when, $else, $token->line); }); @@ -256,7 +256,7 @@ public function __construct() { $statements= $this->statements($parse); $parse->expecting('}', 'arrow function'); } else { - $statements= $this->expressionWithThrows($parse, 0); + $statements= $this->expression($parse, 0); } return new LambdaExpression($signature, $statements, $token->line); @@ -345,7 +345,7 @@ public function __construct() { $statements= $this->statements($parse); $parse->expecting('}', 'fn'); } else { - $statements= $this->expressionWithThrows($parse, 0); + $statements= $this->expression($parse, 0); } return new LambdaExpression($signature, $statements, $token->line); @@ -390,7 +390,7 @@ public function __construct() { if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' $parse->forward(); - $expr= $this->expressionWithThrows($parse, 0); + $expr= $this->expression($parse, 0); $statements= [new ReturnStatement($expr, $token->line)]; $parse->expecting(';', 'function'); } else { // Regular function @@ -906,7 +906,7 @@ public function __construct() { $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); $parse->forward(); $line= $parse->token->line; - $expr= $this->expressionWithThrows($parse, 0); + $expr= $this->expression($parse, 0); $statements= [new ReturnStatement($expr, $line)]; $parse->expecting(';', 'method declaration'); } else { @@ -1251,14 +1251,4 @@ public function expressions($parse, $end= ')') { } return $arguments; } - - public function expressionWithThrows($parse, $bp) { - if ('throw' === $parse->token->value) { - $line= $parse->token->line; - $parse->forward(); - return new ThrowExpression($this->expression($parse, $bp), $line); - } else { - return $this->expression($parse, $bp); - } - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php index 47382794..129dd332 100755 --- a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php +++ b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php @@ -43,7 +43,7 @@ public function setup($parser, $emitter) { $signature= $this->signature($parse); $parse->expecting('=>', 'compact function'); - $return= new ReturnStatement($this->expressionWithThrows($parse, 0), $parse->token->line); + $return= new ReturnStatement($this->expression($parse, 0), $parse->token->line); $parse->expecting(';', 'compact function'); $body[$lookup]= new Method($modifiers, $name, $signature, [$return], $annotations, $comment, $line); diff --git a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php index f8a13157..c94aca00 100755 --- a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php +++ b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php @@ -24,7 +24,7 @@ public function setup($parser, $emitter) { $statements= $this->statements($parse); $parse->expecting('}', 'arrow function'); } else { - $statements= $this->expressionWithThrows($parse, 0); + $statements= $this->expression($parse, 0); } return new LambdaExpression($signature, $statements, $node->line); diff --git a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php new file mode 100755 index 00000000..9ecc46cb --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php @@ -0,0 +1,13 @@ +prefix('throw', function($parse, $token) { + return new ThrowExpression($this->expression($parse, 0), $token->line); + }); + } +} \ No newline at end of file From 65b58a0c8012d46067731fd795a00286bcd56cce Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 16:10:15 +0200 Subject: [PATCH 119/926] QA: Document throw expressions --- .../lang/ast/syntax/php/ThrowExpressions.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php index 9ecc46cb..66414e09 100755 --- a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php +++ b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php @@ -3,6 +3,19 @@ use lang\ast\nodes\ThrowExpression; use lang\ast\syntax\Extension; +/** + * Throw expressions + * + * ```php + * // Syntax + * return $user ?? throw new IllegalStateException('No such user'); + * + * // Rewritten to + * return $user ?? (function() { throw ... })(); + * ``` + * @see https://github.com/xp-framework/compiler/pull/53 + * @test xp://lang.ast.unittest.emit.ExceptionsTest + */ class ThrowExpressions implements Extension { public function setup($language, $emitter) { From 06d694b3c289ad5622cc86e6d46e1fec1855bc62 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 16:13:15 +0200 Subject: [PATCH 120/926] Remove no-longer used infixt() --- src/main/php/lang/ast/Language.class.php | 7 ------- src/main/php/lang/ast/syntax/PHP.class.php | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 454709bb..bef5fc16 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -59,13 +59,6 @@ public function infixr($id, $bp, $led= null) { }; } - public function infixt($id, $bp) { - $infix= $this->symbol($id, $bp); - $infix->led= function($parse, $token, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $token->line); - }; - } - public function prefix($id, $nud= null) { $prefix= $this->symbol($id); $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id) { diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 00a14bf5..a55341eb 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -87,8 +87,8 @@ public function __construct() { $this->constant('false', 'false'); $this->constant('null', 'null'); - $this->infixt('??', 30); - $this->infixt('?:', 30); + $this->infixr('??', 30); + $this->infixr('?:', 30); $this->infixr('&&', 30); $this->infixr('||', 30); From 289bf5df17913d12b4ccc330b4b535e5fda1252b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 16:31:09 +0200 Subject: [PATCH 121/926] Pass kind (prefix, suffix) to UnaryExpression constructor --- src/main/php/lang/ast/Language.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP.class.php | 7 ++++++- .../php/lang/ast/unittest/emit/LoopsTest.class.php | 6 +++--- .../php/lang/ast/unittest/parse/LoopsTest.class.php | 2 +- .../php/lang/ast/unittest/parse/OperatorTest.class.php | 10 +++++----- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index bef5fc16..302e2e50 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -62,14 +62,15 @@ public function infixr($id, $bp, $led= null) { public function prefix($id, $nud= null) { $prefix= $this->symbol($id); $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id) { - return new UnaryExpression($this->expression($parse, 0), $id, $token->line); + return new UnaryExpression('prefix', $this->expression($parse, 0), $id, $token->line); }; } public function suffix($id, $bp, $led= null) { $suffix= $this->symbol($id, $bp); $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id) { - return new UnaryExpression($left, $id, $token->line); + $expr= new UnaryExpression('suffix', $left, $id, $token->line); + return $expr; }; } diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 988906c3..8058e7de 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -438,11 +438,16 @@ protected function emitBinary($result, $binary) { $this->emitOne($result, $binary->right); } - protected function emitUnary($result, $unary) { + protected function emitPrefix($result, $unary) { $result->out->write($unary->operator); $this->emitOne($result, $unary->expression); } + protected function emitSuffix($result, $unary) { + $this->emitOne($result, $unary->expression); + $result->out->write($unary->operator); + } + protected function emitTernary($result, $ternary) { $this->emitOne($result, $ternary->condition); $result->out->write('?'); diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index c0b0f7fd..31f039a7 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -73,7 +73,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + $this->assertEquals('1,2,3,4', $r); } #[@test] @@ -89,7 +89,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + $this->assertEquals('1,2,3,4', $r); } #[@test] @@ -118,7 +118,7 @@ public function continue_while_loop() { public function run() { $i= 0; $r= []; - while ($i++ < 5) { + while (++$i < 5) { if (1 === $i) { continue; } else { diff --git a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php index 32c70813..94ab4dec 100755 --- a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php @@ -71,7 +71,7 @@ public function for_loop() { [new ForLoop( [new Assignment(new Variable('i', self::LINE), '=', new Literal('0', self::LINE), self::LINE)], [new BinaryExpression(new Variable('i', self::LINE), '<', new Literal('10', self::LINE), self::LINE)], - [new UnaryExpression(new Variable('i', self::LINE), '++', self::LINE)], + [new UnaryExpression('suffix', new Variable('i', self::LINE), '++', self::LINE)], [$this->loop], self::LINE )], diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index a09d7221..0f213e39 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -53,7 +53,7 @@ public function comparison($operator) { #[@test, @values(['++', '--'])] public function suffix($operator) { $this->assertParsed( - [new UnaryExpression(new Variable('a', self::LINE), $operator, self::LINE)], + [new UnaryExpression('suffix', new Variable('a', self::LINE), $operator, self::LINE)], '$a'.$operator.';' ); } @@ -61,7 +61,7 @@ public function suffix($operator) { #[@test, @values(['!', '~', '-', '+', '++', '--'])] public function prefix($operator) { $this->assertParsed( - [new UnaryExpression(new Variable('a', self::LINE), $operator, self::LINE)], + [new UnaryExpression('prefix', new Variable('a', self::LINE), $operator, self::LINE)], ''.$operator.'$a;' ); } @@ -123,7 +123,7 @@ public function append_array() { #[@test] public function clone_expression() { $this->assertParsed( - [new UnaryExpression(new Variable('a', self::LINE), 'clone', self::LINE)], + [new UnaryExpression('prefix', new Variable('a', self::LINE), 'clone', self::LINE)], 'clone $a;' ); } @@ -131,7 +131,7 @@ public function clone_expression() { #[@test] public function error_suppression() { $this->assertParsed( - [new UnaryExpression(new Variable('a', self::LINE), '@', self::LINE)], + [new UnaryExpression('prefix', new Variable('a', self::LINE), '@', self::LINE)], '@$a;' ); } @@ -139,7 +139,7 @@ public function error_suppression() { #[@test] public function reference() { $this->assertParsed( - [new UnaryExpression(new Variable('a', self::LINE), '&', self::LINE)], + [new UnaryExpression('prefix', new Variable('a', self::LINE), '&', self::LINE)], '&$a;' ); } From 150c01ff2eb055c8c1ead71a8d189cfde5cad7e5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 16:37:13 +0200 Subject: [PATCH 122/926] Use now-released AST library 3.0.0 https://github.com/xp-framework/ast/releases/tag/v3.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 281d1c06..371a6cde 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "dev-master as 3.0.0", + "xp-framework/ast": "^3.0", "php" : ">=5.6.0" }, "require-dev" : { From bf10ae0d5074045728dfb3eb1e0f1a6ed8c1efc1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 17:43:33 +0200 Subject: [PATCH 123/926] Document Syntax plugins (PR #66) --- ChangeLog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index cc3e4bf7..79de6b3a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,15 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 4.0.0 / ????-??-?? + +* Merged PR #66: Syntax plugins. With this facility in place, the compiler + can be extended much like [Babel](https://babeljs.io/docs/en/plugins). + This is useful for adapting features which may or may not make it into + PHP one day. Current extensions like compact methods are kept for BC + reasons, but will be extracted into their own libraries in the future! + (@thekid) + ## 3.0.0 / 2019-08-10 * Made compatible with PHP 7.4 - refrain using `{}` for string offsets From 58c671c8c493526a16351c8b663c499b9cd10c59 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 20:07:47 +0200 Subject: [PATCH 124/926] Fix operator precedence for unary prefix operators --- ChangeLog.md | 1 + src/main/php/lang/ast/Language.class.php | 7 +-- src/main/php/lang/ast/syntax/PHP.class.php | 46 +++++++++---------- .../ast/syntax/php/ThrowExpressions.class.php | 2 +- .../unittest/emit/PrecedenceTest.class.php | 32 +++++++++++++ .../ast/unittest/parse/OperatorTest.class.php | 27 +++++++++++ 6 files changed, 88 insertions(+), 27 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 79de6b3a..6e370076 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 4.0.0 / ????-??-?? +* Fixed operator precedence for unary prefix operators - @thekid * Merged PR #66: Syntax plugins. With this facility in place, the compiler can be extended much like [Babel](https://babeljs.io/docs/en/plugins). This is useful for adapting features which may or may not make it into diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php index 302e2e50..cdb57669 100755 --- a/src/main/php/lang/ast/Language.class.php +++ b/src/main/php/lang/ast/Language.class.php @@ -59,10 +59,11 @@ public function infixr($id, $bp, $led= null) { }; } - public function prefix($id, $nud= null) { + public function prefix($id, $bp, $nud= null) { $prefix= $this->symbol($id); - $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id) { - return new UnaryExpression('prefix', $this->expression($parse, 0), $id, $token->line); + $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id, $bp) { + $expr= $this->expression($parse, $bp); + return new UnaryExpression('prefix', $expr, $id, $token->line); }; } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index a55341eb..6a99f89e 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -119,7 +119,7 @@ public function __construct() { $this->infixr('<<', 70); $this->infixr('>>', 70); - $this->infix('instanceof', 60, function($parse, $token, $left) { + $this->infix('instanceof', 110, function($parse, $token, $left) { if ('name' === $parse->token->kind) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); @@ -189,15 +189,15 @@ public function __construct() { return new TernaryExpression($left, $when, $else, $token->line); }); - $this->prefix('@'); - $this->prefix('&'); - $this->prefix('~'); - $this->prefix('!'); - $this->prefix('+'); - $this->prefix('-'); - $this->prefix('++'); - $this->prefix('--'); - $this->prefix('clone'); + $this->prefix('@', 100); + $this->prefix('&', 100); + $this->prefix('!', 100); + $this->prefix('~', 100); + $this->prefix('+', 100); + $this->prefix('-', 100); + $this->prefix('++', 100); + $this->prefix('--', 100); + $this->prefix('clone', 100); $this->assignment('='); $this->assignment('&='); @@ -220,7 +220,7 @@ public function __construct() { // - A cast `(int)$a` or `(int)($a / 2)`. // // Resolve by looking ahead after the closing ")" - $this->prefix('(', function($parse, $token) { + $this->prefix('(', 0, function($parse, $token) { static $types= [ '<' => true, '>' => true, @@ -275,7 +275,7 @@ public function __construct() { } }); - $this->prefix('[', function($parse, $token) { + $this->prefix('[', 0, function($parse, $token) { $values= []; while (']' !== $parse->token->value) { $expr= $this->expression($parse, 0); @@ -298,7 +298,7 @@ public function __construct() { return new ArrayLiteral($values, $token->line); }); - $this->prefix('new', function($parse, $token) { + $this->prefix('new', 0, function($parse, $token) { $type= $parse->token; $parse->forward(); @@ -315,7 +315,7 @@ public function __construct() { } }); - $this->prefix('yield', function($parse, $token) { + $this->prefix('yield', 0, function($parse, $token) { if (';' === $parse->token->value) { return new YieldExpression(null, null, $token->line); } else if ('from' === $parse->token->value) { @@ -332,11 +332,11 @@ public function __construct() { } }); - $this->prefix('...', function($parse, $token) { + $this->prefix('...', 0, function($parse, $token) { return new UnpackExpression($this->expression($parse, 0), $token->line); }); - $this->prefix('fn', function($parse, $token) { + $this->prefix('fn', 0, function($parse, $token) { $signature= $this->signature($parse); $parse->expecting('=>', 'fn'); @@ -351,7 +351,7 @@ public function __construct() { return new LambdaExpression($signature, $statements, $token->line); }); - $this->prefix('function', function($parse, $token) { + $this->prefix('function', 0, function($parse, $token) { // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; // the latter explicitely becomes a statement by pushing a semicolon. @@ -405,7 +405,7 @@ public function __construct() { } }); - $this->prefix('static', function($parse, $token) { + $this->prefix('static', 0, function($parse, $token) { if ('variable' === $parse->token->kind) { $init= []; while (';' !== $parse->token->value) { @@ -428,13 +428,13 @@ public function __construct() { return new Literal($token->value, $token->line); }); - $this->prefix('goto', function($parse, $token) { + $this->prefix('goto', 0, function($parse, $token) { $label= $parse->token->value; $parse->forward(); return new GotoStatement($label, $token->line); }); - $this->prefix('(name)', function($parse, $token) { + $this->prefix('(name)', 0, function($parse, $token) { if (':' === $parse->token->value) { $parse->token= new Token($this->symbol(';')); return new Label($token->value, $token->line); @@ -443,12 +443,12 @@ public function __construct() { } }); - $this->prefix('(variable)', function($parse, $token) { + $this->prefix('(variable)', 0, function($parse, $token) { return new Variable($token->value, $token->line); }); - $this->prefix('(literal)', function($parse, $token) { + $this->prefix('(literal)', 0, function($parse, $token) { return new Literal($token->value, $token->line); }); @@ -464,7 +464,7 @@ public function __construct() { return new Block($statements, $token->line); }); - $this->prefix('echo', function($parse, $token) { + $this->prefix('echo', 0, function($parse, $token) { return new EchoStatement($this->expressions($parse, ';'), $token->line); }); diff --git a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php index 66414e09..d05d75c2 100755 --- a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php +++ b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php @@ -19,7 +19,7 @@ class ThrowExpressions implements Extension { public function setup($language, $emitter) { - $language->prefix('throw', function($parse, $token) { + $language->prefix('throw', 0, function($parse, $token) { return new ThrowExpression($this->expression($parse, 0), $token->line); }); } diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php new file mode 100755 index 00000000..cf121551 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -0,0 +1,32 @@ +assertEquals($result, $this->run( + 'class { + public function run() { + return '.$input.'; + } + }' + )); + } + + #[@test] + public function concatenation() { + $t= $this->type( + 'class { + public function run() { + return "(".self::class.")"; + } + }' + ); + $this->assertEquals('('.$t->getName().')', $t->newinstance()->run()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index 0f213e39..42ad571e 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -6,6 +6,7 @@ use lang\ast\nodes\Braced; use lang\ast\nodes\ClassDeclaration; use lang\ast\nodes\InstanceExpression; +use lang\ast\nodes\InstanceOfExpression; use lang\ast\nodes\Literal; use lang\ast\nodes\NewClassExpression; use lang\ast\nodes\NewExpression; @@ -203,4 +204,30 @@ public function precedence_of_scope_resolution_operator() { 'self::class."test";' ); } + + #[@test] + public function precedence_of_not_and_instance_of() { + $this->assertParsed( + [new UnaryExpression( + 'prefix', + new InstanceOfExpression(new Variable('this', self::LINE), 'self', self::LINE), + '!', + self::LINE + )], + '!$this instanceof self;' + ); + } + + #[@test, @values(['+', '-', '~'])] + public function precedence_of_prefix($operator) { + $this->assertParsed( + [new BinaryExpression( + new UnaryExpression('prefix', new Literal('2', self::LINE), $operator, self::LINE), + '===', + new Variable('value', self::LINE), + self::LINE + )], + $operator.'2 === $value;' + ); + } } \ No newline at end of file From 672072c98942b14b1f27f2f5ae78db084227f096 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 21:24:05 +0200 Subject: [PATCH 125/926] Lazily initialize language implementations --- src/main/php/lang/ast/Compiled.class.php | 27 ++++++++++++++----- .../lang/ast/CompilingClassloader.class.php | 12 ++------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index da7bbc88..efb787f7 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -4,18 +4,31 @@ use text\StreamTokenizer; class Compiled implements OutputStream { - public static $source= [], $emit= [], $lang; + public static $source= [], $emit= [], $lang= []; private $compiled= '', $offset= 0; public static function bytes($version, $source, $file) { - $stream= $source->getResourceAsStream($file); - return self::parse($version, $stream->in(), new self(), $file)->compiled; + $stream= $source[1]->getResourceAsStream($file); + return self::parse($source[0], $stream->in(), $version, new self(), $file)->compiled; } - private static function parse($version, $in, $out, $file) { + private static function language($name, $emitter) { + $language= Language::named(strtoupper($name)); + foreach ($language->extensions() as $extension) { + $extension->setup($language, $emitter); + } + return $language; + } + + private static function parse($lang, $in, $version, $out, $file) { + $language= isset(self::$lang[$lang]) + ? self::$lang[$lang] + : self::$lang[$lang]= self::language($lang, self::$emit[$version]) + ; + try { - $parse= new Parse(self::$lang, new Tokens(new StreamTokenizer($in)), $file); + $parse= new Parse($language, new Tokens(new StreamTokenizer($in)), $file); self::$emit[$version]->emitAll(new Result($out), $parse->execute()); return $out; } finally { @@ -33,8 +46,8 @@ private static function parse($version, $in, $out, $file) { */ public function stream_open($path, $mode, $options, &$opened) { list($version, $file)= explode('://', $path); - $stream= self::$source[$file]->getResourceAsStream($file); - self::parse($version, $stream->in(), $this, $file); + $stream= self::$source[$file][1]->getResourceAsStream($file); + self::parse(self::$source[$file][0], $stream->in(), $version, $this, $file); $opened= $stream->getURI(); return true; } diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 5beb12e5..8969c09b 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -19,15 +19,7 @@ class CompilingClassLoader implements IClassLoader { /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { $this->version= $emit->getSimpleName(); - - $emitter= $emit->newInstance(); - $language= Language::named('PHP'); - foreach ($language->extensions() as $extension) { - $extension->setup($language, $emitter); - } - - Compiled::$emit[$this->version]= $emitter; - Compiled::$lang= $language; + Compiled::$emit[$this->version]= $emit->newInstance(); stream_wrapper_register($this->version, Compiled::class); } @@ -42,7 +34,7 @@ protected function locateSource($class) { $uri= strtr($class, '.', '/').self::EXTENSION; foreach (ClassLoader::getDefault()->getLoaders() as $loader) { if ($loader instanceof self) continue; - if ($loader->providesResource($uri)) return $this->source[$class]= $loader; + if ($loader->providesResource($uri)) return $this->source[$class]= [substr(self::EXTENSION, 1), $loader]; } return null; } From 013e52d22307b83b4d26ce27897d2681f86e67be Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 21:46:32 +0200 Subject: [PATCH 126/926] Remove unused member --- src/main/php/lang/ast/CompilingClassloader.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 8969c09b..9241f7f5 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -13,7 +13,6 @@ class CompilingClassLoader implements IClassLoader { const EXTENSION = '.php'; private static $instance= []; - public static $syntax= []; private $version; /** Creates a new instances with a given PHP runtime */ From a91ad4391b858cc9cf6894f7da0a008cfc8cc87e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 21:48:58 +0200 Subject: [PATCH 127/926] Separate language extensions by class loader --- src/main/php/xp/compiler/Usage.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 6fc5a2bc..5326f435 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -8,7 +8,6 @@ class Usage { /** @return int */ public static function main(array $args) { Console::$err->writeLine('Usage: xp compile []'); - Console::$err->writeLine(); // Show syntax implementations sorted by class loader $loaders= $sorted= []; @@ -24,6 +23,7 @@ public static function main(array $args) { } } foreach ($sorted as $hash => $list) { + Console::$err->writeLine(); Console::$err->writeLine("\033[33m@", $loaders[$hash], "\033[0m"); foreach ($list as $syntax) { Console::$err->writeLine($syntax->getName()); From 0f02f5c06a8f673b1fdccbca9940b89f7bdac694 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 21:56:15 +0200 Subject: [PATCH 128/926] Document plugins, linking to https://github.com/xp-lang [skip ci] --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index dd5880e1..dc7449a4 100755 --- a/README.md +++ b/README.md @@ -42,6 +42,26 @@ Features supported XP Compiler supports features such as annotations, arrow functions, property type-hints, the null-safe instance operator as well as all PHP 7 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). +Additional syntax can be added by installing compiler plugins from [here](https://github.com/xp-lang): + +```bash +$ composer require xp-lang/php-is-operator +# ... + +$ xp compile +Usage: xp compile [] + +@FileSystemCL<./vendor/xp-framework/compiler/src/main/php> +lang.ast.syntax.TransformationApi +lang.ast.syntax.php.CompactMethods +lang.ast.syntax.php.HackArrowFunctions +lang.ast.syntax.php.NullSafe +lang.ast.syntax.php.ThrowExpressions +lang.ast.syntax.php.Using + +@FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> +lang.ast.syntax.php.IsOperator +``` Implementation status --------------------- From 3c4df23c322bccc097aac3068429c32ecb942ec9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 22:56:53 +0200 Subject: [PATCH 129/926] Remove support for Hack arrow functions Deprecated in favor of https://wiki.php.net/rfc/arrow_functions_v2 --- README.md | 1 - src/main/php/lang/ast/Tokens.class.php | 2 +- src/main/php/lang/ast/syntax/PHP.class.php | 39 +++------------- .../syntax/php/HackArrowFunctions.class.php | 33 -------------- .../lang/ast/unittest/TokensTest.class.php | 1 - .../emit/CompactFunctionsTest.class.php | 9 ---- .../ast/unittest/emit/LambdasTest.class.php | 44 ------------------- .../parse/CompactFunctionsTest.class.php | 13 +----- .../ast/unittest/parse/LambdasTest.class.php | 6 +-- 9 files changed, 10 insertions(+), 138 deletions(-) delete mode 100755 src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php diff --git a/README.md b/README.md index dc7449a4..f0109481 100755 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ Usage: xp compile [] @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> lang.ast.syntax.TransformationApi lang.ast.syntax.php.CompactMethods -lang.ast.syntax.php.HackArrowFunctions lang.ast.syntax.php.NullSafe lang.ast.syntax.php.ThrowExpressions lang.ast.syntax.php.Using diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index a9fccd4c..634b8d70 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -9,7 +9,7 @@ class Tokens implements \IteratorAggregate { private static $operators= [ '<' => ['<=', '<<', '<>', '', '<<='], '>' => ['>=', '>>', '>>='], - '=' => ['=>', '==', '==>', '==='], + '=' => ['=>', '==', '==='], '!' => ['!=', '!=='], '&' => ['&&', '&='], '|' => ['||', '|='], diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 6a99f89e..5003adc2 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -245,22 +245,7 @@ public function __construct() { } $parse->queue= array_merge($skipped, $parse->queue); - if (':' === $parse->token->value || '==>' === $parse->token->value) { - $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - $parse->forward(); - $signature= $this->signature($parse); - $parse->forward(); - - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'arrow function'); - } else { - $statements= $this->expression($parse, 0); - } - - return new LambdaExpression($signature, $statements, $token->line); - } else if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { + if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { $parse->forward(); $parse->expecting('(', 'cast'); $type= $this->type0($parse, false); @@ -388,16 +373,9 @@ public function __construct() { $parse->forward(); $signature= $this->signature($parse); - if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' - $parse->forward(); - $expr= $this->expression($parse, 0); - $statements= [new ReturnStatement($expr, $token->line)]; - $parse->expecting(';', 'function'); - } else { // Regular function - $parse->expecting('{', 'function'); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); - } + $parse->expecting('{', 'function'); + $statements= $this->statements($parse); + $parse->expecting('}', 'function'); $parse->queue= [$parse->token]; $parse->token= new Token($this->symbol(';')); @@ -902,15 +880,8 @@ public function __construct() { } else if (';' === $parse->token->value) { // Abstract or interface method $statements= null; $parse->expecting(';', 'method declaration'); - } else if ('==>' === $parse->token->value) { // Compact syntax, terminated with ';' - $parse->warn('Hack language style compact functions are deprecated, please use `fn` syntax instead'); - $parse->forward(); - $line= $parse->token->line; - $expr= $this->expression($parse, 0); - $statements= [new ReturnStatement($expr, $line)]; - $parse->expecting(';', 'method declaration'); } else { - $parse->expecting('{, ; or ==>', 'method declaration'); + $parse->expecting('{ or ;', 'method declaration'); } $body[$lookup]= new Method($modifiers, $name, $signature, $statements, $annotations, $comment, $line); diff --git a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php b/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php deleted file mode 100755 index c94aca00..00000000 --- a/src/main/php/lang/ast/syntax/php/HackArrowFunctions.class.php +++ /dev/null @@ -1,33 +0,0 @@ -infix('==>', 80, function($parse, $node, $left) { - $parse->warn('Hack language style arrow functions are deprecated, please use `fn` syntax instead'); - - $signature= new Signature([new Parameter($left->name, null)], null); - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'arrow function'); - } else { - $statements= $this->expression($parse, 0); - } - - return new LambdaExpression($signature, $statements, $node->line); - }); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php index ec3baa3c..6d94fd11 100755 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ b/src/test/php/lang/ast/unittest/TokensTest.class.php @@ -76,7 +76,6 @@ public function variables($input) { # '<=', '>=', '<=>', # '===', '!==', # '=>', - # '==>', # '->', #])] public function operators($input) { diff --git a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php index cfafabfc..7b2df15b 100755 --- a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php @@ -38,13 +38,4 @@ public function run() { }'); $this->assertEquals('test', $r); } - - #[@test] - public function hacklang_variation_also_supported() { - $r= $this->run('class { - public function run() ==> "test"; - }'); - $this->assertEquals('test', $r); - \xp::gc(); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index a1366d69..13033cd0 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -5,14 +5,7 @@ /** * Lambdas (a.k.a. arrow functions) support * - * NOTE: Like in Hack, all captured variables are captured by value; - * capturing by reference is not supported. If you need to capture by - * reference, use a full PHP closure. - * * @see https://wiki.php.net/rfc/arrow_functions_v2 - * @see https://docs.hhvm.com/hack/operators/lambda - * @see https://docs.hhvm.com/hack/lambdas/introduction - * @see https://wiki.php.net/rfc/arrow_functions (Under Discussion) */ class LambdasTest extends EmittingTest { @@ -183,41 +176,4 @@ public function run() { $this->assertEquals(2, $r()); } - - #[@test] - public function hack_variant_also_supportted() { - $r= $this->run('class { - public function run() { - return ($a) ==> $a + 1; - } - }'); - - $this->assertEquals(2, $r(1)); - \xp::gc(); - } - - #[@test] - public function hack_variant_without_braces() { - $r= $this->run('class { - public function run() { - return $a ==> $a + 1; - } - }'); - - $this->assertEquals(2, $r(1)); - \xp::gc(); - } - - #[@test] - public function hack_variant_without_braces_as_argument() { - $r= $this->run('class { - private function apply($f, ... $args) { return $f(...$args); } - public function run() { - return $this->apply($a ==> $a + 1, 2); - } - }'); - - $this->assertEquals(3, $r); - \xp::gc(); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php index e88f3ec8..a703ba0f 100755 --- a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php @@ -1,7 +1,6 @@ return= new ReturnStatement(new Literal('null', self::LINE), self::LINE); } - #[@test] - public function compact_function() { - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [$this->return], self::LINE)], - 'function a() ==> null;' - ); - \xp::gc(); - } - #[@test] public function compact_method() { $method= new Method(['public'], 'a', new Signature([], null), [$this->return], [], null, self::LINE); $this->assertParsed( [new ClassDeclaration([], '\\A', null, [], [$method->lookup() => $method], [], null, self::LINE)], - 'class A { public function a() ==> null; }' + 'class A { public fn a() => null; }' ); - \xp::gc(); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php index eacf6ad7..14155c68 100755 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php @@ -21,7 +21,7 @@ public function setUp() { public function short_closure() { $this->assertParsed( [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], - '($a) ==> $a + 1;' + 'fn($a) => $a + 1;' ); \xp::gc(); } @@ -34,7 +34,7 @@ public function short_closure_as_arg() { [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], self::LINE )], - 'execute(($a) ==> $a + 1);' + 'execute(fn($a) => $a + 1);' ); \xp::gc(); } @@ -43,7 +43,7 @@ public function short_closure_as_arg() { public function short_closure_with_braces() { $this->assertParsed( [new LambdaExpression(new Signature([new Parameter('a', null)], null), [new ReturnStatement($this->expression, self::LINE)], self::LINE)], - '($a) ==> { return $a + 1; };' + 'fn($a) => { return $a + 1; };' ); \xp::gc(); } From e909664f3ad8c13e95cd712d9da4d6d786c44413 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:02:19 +0200 Subject: [PATCH 130/926] Document Hack arrow functions removal --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6e370076..851fbeef 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 4.0.0 / ????-??-?? +* Merged PR #69: Remove support for Hack arrow functions - @thekid * Fixed operator precedence for unary prefix operators - @thekid * Merged PR #66: Syntax plugins. With this facility in place, the compiler can be extended much like [Babel](https://babeljs.io/docs/en/plugins). From 67d11b9f1840dc5bd183face1e553eec895b7497 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:10:57 +0200 Subject: [PATCH 131/926] Prepare 4.0.0-RELEASE --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 851fbeef..4c06c8df 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 4.0.0 / ????-??-?? +## 4.0.0 / 2019-09-09 * Merged PR #69: Remove support for Hack arrow functions - @thekid * Fixed operator precedence for unary prefix operators - @thekid From e4abb71a97cadd16ff13e715498466e919d4fe4b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:47:45 +0200 Subject: [PATCH 132/926] Remove compact functions and corresponding tests --- .../ast/syntax/php/CompactMethods.class.php | 52 ------------------- .../emit/CompactFunctionsTest.class.php | 41 --------------- .../parse/CompactFunctionsTest.class.php | 26 ---------- 3 files changed, 119 deletions(-) delete mode 100755 src/main/php/lang/ast/syntax/php/CompactMethods.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php diff --git a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php b/src/main/php/lang/ast/syntax/php/CompactMethods.class.php deleted file mode 100755 index 129dd332..00000000 --- a/src/main/php/lang/ast/syntax/php/CompactMethods.class.php +++ /dev/null @@ -1,52 +0,0 @@ - $this->name; - * } - * - * // Rewritten to - * class Person { - * private string $name; - * public function name(): string { return $this->name; } - * } - * ``` - * @see https://github.com/xp-framework/rfc/issues/241 - * @test xp://lang.ast.unittest.emit.CompactFunctionsTest - */ -class CompactMethods implements Extension { - - public function setup($parser, $emitter) { - $parser->body('fn', function($parse, &$body, $annotations, $modifiers) { - $line= $parse->token->line; - $comment= $parse->comment; - $parse->comment= null; - - $parse->forward(); - $name= $parse->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $parse->raise('Cannot redeclare method '.$lookup); - } - - $parse->forward(); - $signature= $this->signature($parse); - - $parse->expecting('=>', 'compact function'); - $return= new ReturnStatement($this->expression($parse, 0), $parse->token->line); - $parse->expecting(';', 'compact function'); - - $body[$lookup]= new Method($modifiers, $name, $signature, [$return], $annotations, $comment, $line); - }); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php deleted file mode 100755 index 7b2df15b..00000000 --- a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php +++ /dev/null @@ -1,41 +0,0 @@ -run('class { - public fn run() => "test"; - }'); - $this->assertEquals('test', $r); - } - - #[@test] - public function with_property() { - $r= $this->run('class { - private $id= "test"; - - public fn run() => $this->id; - }'); - $this->assertEquals('test', $r); - } - - #[@test] - public function combined_with_argument_promotion() { - $r= $this->run('class { - public fn withId(private $id) => $this; - public fn id() => $this->id; - - public function run() { - return $this->withId("test")->id(); - } - }'); - $this->assertEquals('test', $r); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php deleted file mode 100755 index a703ba0f..00000000 --- a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php +++ /dev/null @@ -1,26 +0,0 @@ -return= new ReturnStatement(new Literal('null', self::LINE), self::LINE); - } - - #[@test] - public function compact_method() { - $method= new Method(['public'], 'a', new Signature([], null), [$this->return], [], null, self::LINE); - - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], [$method->lookup() => $method], [], null, self::LINE)], - 'class A { public fn a() => null; }' - ); - } -} \ No newline at end of file From f305ced9ae108922f668e84b7c5b262adc563ffe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:48:02 +0200 Subject: [PATCH 133/926] Refrain from using compact functions --- .../lang/ast/unittest/emit/EchoTest.class.php | 2 +- .../unittest/emit/ExceptionsTest.class.php | 52 ------------------- .../ast/unittest/emit/NullSafeTest.class.php | 2 +- .../ast/unittest/emit/VarargsTest.class.php | 8 ++- 4 files changed, 8 insertions(+), 56 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index b9e25fdf..5f1feb36 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -14,7 +14,7 @@ private function assertEchoes($expected, $statement) { ob_start(); try { $this->run('class { - private fn hello() => "Hello"; + private function hello() { return "Hello"; } public function run() { '.$statement.' } }'); $this->assertEquals($expected, ob_get_contents()); diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 21a219c5..2ff9ac97 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -118,56 +118,4 @@ public function run($user) { }'); $t->newInstance()->run(null); } - - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_lambda() { - $t= $this->type('class { - public function run() { - $f= fn() => throw new \\lang\\IllegalArgumentException("test"); - $f(); - } - }'); - $t->newInstance()->run(); - } - - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_lambda_throwing_variable() { - $t= $this->type('class { - public function run($e) { - $f= fn() => throw $e; - $f(); - } - }'); - $t->newInstance()->run(new IllegalArgumentException('Test')); - } - - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_lambda_capturing_variable() { - $t= $this->type('class { - public function run() { - $f= fn($message) => throw new \\lang\\IllegalArgumentException($message); - $f("test"); - } - }'); - $t->newInstance()->run(); - } - - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_lambda_capturing_parameter() { - $t= $this->type('class { - public function run($message) { - $f= fn() => throw new \\lang\\IllegalArgumentException($message); - $f(); - } - }'); - $t->newInstance()->run('Test'); - } - - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_compact_method() { - $t= $this->type('class { - public fn run() => throw new \\lang\\IllegalArgumentException("test"); - }'); - $t->newInstance()->run(); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index cf65dc9f..bc7457ba 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -92,7 +92,7 @@ public function run() { public $member= true; }; $member= new class() { - public fn name() => "member"; + public function name() { return "member"; } }; return $object?->{$member->name()}; } diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index cfdb4555..42740a56 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -5,7 +5,9 @@ class VarargsTest extends EmittingTest { #[@test] public function vsprintf() { $r= $this->run('class { - private fn format(string $format, ... $args) => vsprintf($format, $args); + private function format(string $format, ... $args) { + return vsprintf($format, $args); + } public function run() { return $this->format("Hello %s", "Test"); @@ -18,7 +20,9 @@ public function run() { #[@test] public function list_of() { $r= $this->run('class { - private fn listOf(string... $args) => $args; + private function listOf(string... $args) { + return $args; + } public function run() { return $this->listOf("Hello", "Test"); From 8269188b83991729d0bad40dabddfd9079a46735 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:48:21 +0200 Subject: [PATCH 134/926] Remove compact functions from documentation --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f0109481..6cf7f6da 100755 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ Usage: xp compile [] @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> lang.ast.syntax.TransformationApi -lang.ast.syntax.php.CompactMethods lang.ast.syntax.php.NullSafe lang.ast.syntax.php.ThrowExpressions lang.ast.syntax.php.Using From faf1746d4bbdc152e713aa031024505ee14bfa3e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 9 Sep 2019 23:54:16 +0200 Subject: [PATCH 135/926] Add back tests for closures and throw expressions --- .../unittest/emit/ExceptionsTest.class.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 2ff9ac97..3c5ab630 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -118,4 +118,46 @@ public function run($user) { }'); $t->newInstance()->run(null); } + + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_lambda() { + $this->run('use lang\IllegalArgumentException; class { + public function run() { + $f= fn() => throw new IllegalArgumentException("test"); + $f(); + } + }'); + } + + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_lambda_throwing_variable() { + $t= $this->type('class { + public function run($e) { + $f= fn() => throw $e; + $f(); + } + }'); + $t->newInstance()->run(new IllegalArgumentException('Test')); + } + + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_lambda_capturing_variable() { + $this->run('use lang\IllegalArgumentException; class { + public function run() { + $f= fn($message) => throw new IllegalArgumentException($message); + $f("test"); + } + }'); + } + + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_lambda_capturing_parameter() { + $t= $this->type('use lang\IllegalArgumentException; class { + public function run($message) { + $f= fn() => throw new IllegalArgumentException($message); + $f(); + } + }'); + $t->newInstance()->run('Test'); + } } \ No newline at end of file From 60b88254eb295f7e519e9dd8ae8fc6dccba29364 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 12:54:35 +0200 Subject: [PATCH 136/926] Implement xp-framework/rfc#334: Drop PHP 5.6 support --- .travis.yml | 1 - ChangeLog.md | 8 ++++++++ README.md | 3 +-- composer.json | 10 +++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3bee7933..b10f30dc 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ language: php dist: trusty php: - - 5.6 - 7.0 - 7.1 - 7.2 diff --git a/ChangeLog.md b/ChangeLog.md index 4c06c8df..ccdd39f3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,14 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.0.0 / ????-??-?? + +* Added support for XP 10 and newer versions of library dependencies + (@thekid) +* Implemented xp-framework/rfc#334: Drop PHP 5.6. The minimum required + PHP version is now 7.0.0! + (@thekid) + ## 4.0.0 / 2019-09-09 * Merged PR #69: Remove support for Hack arrow functions - @thekid diff --git a/README.md b/README.md index f0109481..b5b53f9d 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ XP Compiler [![Build Status on TravisCI](https://secure.travis-ci.org/xp-forge/sequence.svg)](http://travis-ci.org/xp-framework/compiler) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) -[![Required PHP 5.6+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-5_6plus.png)](http://php.net/) [![Supports PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.png)](http://php.net/) [![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) @@ -16,7 +15,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 5.6. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php =5.6.0" + "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", + "xp-framework/tokenize": "^9.0 | ^8.1", + "xp-framework/ast": "^4.0 | ^3.0", + "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/unittest": "^9.3" + "xp-framework/unittest": "^10.0 | ^9.3" }, "bin": ["bin/xp.xp-framework.compiler.compile"], "autoload" : { From ef99981f497147b2bffaf63c345a23e5af6142e3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 12:55:06 +0200 Subject: [PATCH 137/926] Remove PHP 5.6 emitter --- src/main/php/lang/ast/emit/PHP56.class.php | 262 --------------------- 1 file changed, 262 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/PHP56.class.php diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php deleted file mode 100755 index 4e81c15e..00000000 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ /dev/null @@ -1,262 +0,0 @@ - 72, - 'void' => 71, - 'iterable' => 71, - 'string' => 70, - 'int' => 70, - 'bool' => 70, - 'float' => 70, - 'mixed' => null, - ]; - private static $keywords= [ - 'callable' => true, - 'class' => true, - 'trait' => true, - 'extends' => true, - 'implements' => true, - 'static' => true, - 'abstract' => true, - 'final' => true, - 'public' => true, - 'protected' => true, - 'private' => true, - 'const' => true, - 'enddeclare' => true, - 'endfor' => true, - 'endforeach' => true, - 'endif' => true, - 'endwhile' => true, - 'and' => true, - 'global' => true, - 'goto' => true, - 'instanceof' => true, - 'insteadof' => true, - 'interface' => true, - 'namespace' => true, - 'new' => true, - 'or' => true, - 'xor' => true, - 'try' => true, - 'use' => true, - 'var' => true, - 'exit' => true, - 'list' => true, - 'clone' => true, - 'include' => true, - 'include_once' => true, - 'throw' => true, - 'array' => true, - 'print' => true, - 'echo' => true, - 'require' => true, - 'require_once' => true, - 'return' => true, - 'else' => true, - 'elseif' => true, - 'default' => true, - 'break' => true, - 'continue' => true, - 'switch' => true, - 'yield' => true, - 'function' => true, - 'if' => true, - 'endswitch' => true, - 'finally' => true, - 'for' => true, - 'foreach' => true, - 'declare' => true, - 'case' => true, - 'do' => true, - 'while' => true, - 'as' => true, - 'catch' => true, - 'die' => true, - 'self' => true, - 'parent' => true - ]; - - - protected function emitLiteral($result, $literal) { - if ('"' === $literal->expression[0]) { - $result->out->write(preg_replace_callback( - '/\\\\u\{([0-9a-f]+)\}/i', - function($matches) { return html_entity_decode('&#'.hexdec($matches[1]).';', ENT_HTML5, \xp::ENCODING); }, - $literal->expression - )); - } else { - $result->out->write($literal->expression); - } - } - - protected function emitCatch($result, $catch) { - if (empty($catch->types)) { - $result->out->write('catch(\\Exception $'.$catch->variable.') {'); - } else { - $last= array_pop($catch->types); - $label= sprintf('c%u', crc32($last)); - foreach ($catch->types as $type) { - $result->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); - } - $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); - } - - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } - - protected function emitBinary($result, $binary) { - if ('??' === $binary->operator) { - $result->out->write('isset('); - $this->emitOne($result, $binary->left); - $result->out->write(') ?'); - $this->emitOne($result, $binary->left); - $result->out->write(' : '); - $this->emitOne($result, $binary->right); - } else if ('<=>' === $binary->operator) { - $l= $result->temp(); - $r= $result->temp(); - $result->out->write('('.$l.'= '); - $this->emitOne($result, $binary->left); - $result->out->write(') < ('.$r.'='); - $this->emitOne($result, $binary->right); - $result->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); - } else { - parent::emitBinary($result, $binary); - } - } - - protected function emitAssignment($result, $assignment) { - if ('??=' === $assignment->operator) { - $result->out->write('isset('); - $this->emitAssign($result, $assignment->variable); - $result->out->write(') ||'); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->expression); - } else { - $this->emitAssign($result, $assignment->variable); - $result->out->write($assignment->operator); - $this->emitOne($result, $assignment->expression); - } - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitInvoke($result, $invoke) { - $expr= $invoke->expression; - if ('braced' === $expr->kind) { - $t= $result->temp(); - $result->out->write('(('.$t.'='); - $this->emitOne($result, $expr->expression); - $result->out->write(') ? '.$t); - $result->out->write('('); - $this->emitArguments($result, $invoke->arguments); - $result->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); - } else if ( - 'scope' === $expr->kind && - 'literal' === $expr->member->kind && - isset(self::$keywords[strtolower($expr->member->expression)]) - ) { - $result->out->write($expr->type.'::{\''.$expr->member->expression.'\'}'); - $result->out->write('('); - $this->emitArguments($result, $invoke->arguments); - $result->out->write(')'); - } else { - parent::emitInvoke($result, $invoke); - } - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitThrowExpression($result, $throw) { - $capture= []; - foreach ($result->codegen->search($throw, 'variable') as $var) { - if (isset($result->locals[$var->name])) { - $capture[$var->name]= true; - } - } - unset($capture['this']); - - $t= $result->temp(); - $result->out->write('(('.$t.'=function()'); - $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $result->out->write('{ throw '); - $this->emitOne($result, $throw->expression); - $result->out->write('; }) ? '.$t.'() : null)'); - } - - protected function emitNewClass($result, $new) { - $result->out->write('\\lang\\ClassLoader::defineType("class©anonymous'.md5(uniqid()).'", ["kind" => "class"'); - $definition= $new->definition; - $result->out->write(', "extends" => '.($definition->parent ? '[\''.$definition->parent.'\']' : 'null')); - $result->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); - $result->out->write(', "use" => []'); - $result->out->write('], \'{'); - $result->out->write(str_replace('\'', '\\\'', $result->buffer(function($result) use($definition) { - foreach ($definition->body as $member) { - $this->emitOne($result, $member); - $result->out->write("\n"); - } - }))); - $result->out->write('}\')->newInstance('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - } - - protected function emitFrom($result, $from) { - $result->out->write('foreach ('); - $this->emitOne($result, $from->iterable); - $result->out->write(' as $key => $val) yield $key => $val;'); - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitMethod($result, $method) { - if (isset(self::$keywords[strtolower($method->name)])) { - $result->call[in_array('static', $method->modifiers)][]= $method->name; - $method->name= '__'.$method->name; - } else if ('__call' === $method->name || '__callStatic' === $method->name) { - $method->name.= '0'; - } - parent::emitMethod($result, $method); - } - - protected function emitClass($result, $class) { - $result->call= [false => [], true => []]; - array_unshift($result->meta, []); - $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $result->out->write(' extends '.$class->parent); - $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); - $result->out->write('{'); - foreach ($class->body as $member) { - $this->emitOne($result, $member); - } - - if ($result->call[false]) { - $result->out->write('function __call($name, $args) {'); - foreach ($result->call[false] as $name) { - $result->out->write('if (\''.$name.'\' === $name) return $this->__'.$name.'(...$args); else '); - } - $result->out->write('return $this->__call0($name, $args); }'); - } - if ($result->call[true]) { - $result->out->write('static function __callStatic($name, $args) {'); - foreach ($result->call[true] as $name) { - $result->out->write('if (\''.$name.'\' === $name) return self::__'.$name.'(...$args); else '); - } - $result->out->write('return self::__callStatic0($name, ...$args); }'); - } - - $result->out->write('static function __init() {'); - $this->emitMeta($result, $class->name, $class->annotations, $class->comment); - $result->out->write('}} '.$class->name.'::__init();'); - } -} \ No newline at end of file From 2b1d6f6d5c232620e6dbeb4050334a2d987f4faa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 12:55:36 +0200 Subject: [PATCH 138/926] Remove HHVM emitter --- src/main/php/lang/ast/emit/HHVM320.class.php | 26 -------------------- 1 file changed, 26 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/HHVM320.class.php diff --git a/src/main/php/lang/ast/emit/HHVM320.class.php b/src/main/php/lang/ast/emit/HHVM320.class.php deleted file mode 100755 index fa4167e2..00000000 --- a/src/main/php/lang/ast/emit/HHVM320.class.php +++ /dev/null @@ -1,26 +0,0 @@ - 72, - 'void' => 71, - 'iterable' => 71, - 'mixed' => null, - ]; - - protected function emitParameter($result, $parameter) { - if ($parameter->variadic) { - $result->out->write('... $'.$parameter->name); - $result->locals[$parameter->name]= true; - } else { - parent::emitParameter($result, $parameter); - } - } -} \ No newline at end of file From d07c7bf11f302e78f0dc3f85a90c899fe03b4c59 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 12:56:45 +0200 Subject: [PATCH 139/926] Update help to no longer reference PHP 5 --- src/main/php/xp/compiler/CompileRunner.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index eaac9f5c..2a540273 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -32,9 +32,9 @@ * ```sh * $ xp compile -n src/main/php/ * ``` - * - Target PHP 5.6 (default target is current PHP version) + * - Target PHP 7.0 (default target is current PHP version) * ```sh - * $ xp compile -t PHP.5.6 HelloWorld.php HelloWorld.class.php + * $ xp compile -t PHP.7.0 HelloWorld.php HelloWorld.class.php * ``` * * The *-o* and *-n* options accept multiple input sources following them. From 8760c091cae3ab85fd401c9161df66ed1b5a3f27 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 12:58:55 +0200 Subject: [PATCH 140/926] Remove HHVM_VERSION check --- src/main/php/module.xp | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 2 +- src/test/php/lang/ast/unittest/EmitterTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 2 +- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 798880e2..96294d10 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -9,7 +9,7 @@ module xp-framework/compiler { /** @return void */ public function initialize() { - $runtime= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; + $runtime= 'PHP.'.PHP_VERSION; ClassLoader::registerLoader(CompilingClassloader::instanceFor($runtime)); if (!interface_exists(\IDisposable::class, false)) { diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 2a540273..f68f0040 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -47,7 +47,7 @@ class CompileRunner { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $target= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; + $target= 'PHP.'.PHP_VERSION; $in= $out= '-'; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 630ffc51..dbddaf3a 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -10,7 +10,7 @@ class EmitterTest extends TestCase { private function newEmitter() { - return Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + return Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); } #[@test] diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 347e9276..4184033b 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -24,7 +24,7 @@ abstract class EmittingTest extends TestCase { public static function setupCompiler() { self::$cl= DynamicClassLoader::instanceFor(self::class); self::$language= Language::named('PHP'); - self::$emitter= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + self::$emitter= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); foreach (self::$language->extensions() as $extension) { $extension->setup(self::$language, self::$emitter); } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 3f2151a1..a33d102e 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -13,7 +13,7 @@ class CompilingClassLoaderTest extends TestCase { private static $runtime; static function __static() { - self::$runtime= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; + self::$runtime= 'PHP.'.PHP_VERSION; } /** From 5a8223a3c441089f526d0c16fc75477ca47b2635 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 13:02:40 +0200 Subject: [PATCH 141/926] Use null-coalesce operator --- src/main/php/lang/ast/Parse.class.php | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/main/php/lang/ast/syntax/PHP.class.php | 5 +---- src/main/php/xp/compiler/CompileRunner.class.php | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php index e6205b83..c23c3151 100755 --- a/src/main/php/lang/ast/Parse.class.php +++ b/src/main/php/lang/ast/Parse.class.php @@ -65,7 +65,7 @@ public function forward() { list($value, $line)= $this->tokens->current(); $this->tokens->next(); if ('name' === $type) { - $t= new Token(isset($this->language->symbols[$value]) ? $this->language->symbols[$value] : $this->language->symbol('(name)')); + $t= new Token($this->language->symbols[$value] ?? $this->language->symbol('(name)')); $t->kind= $type; } else if ('operator' === $type) { $t= new Token($this->language->symbol($value)); diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8058e7de..885597ff 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -388,7 +388,7 @@ protected function emitMethod($result, $method) { $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', - DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], + DETAIL_ANNOTATIONS => $method->annotations ?? [], DETAIL_COMMENT => $method->comment, DETAIL_TARGET_ANNO => [], DETAIL_ARGUMENTS => [] diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 5003adc2..3cd4847a 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1098,10 +1098,7 @@ public function typeBody($parse) { if (isset($modifier[$parse->token->value])) { $modifiers[]= $parse->token->value; $parse->forward(); - } else if (isset($this->body[$k= $parse->token->value]) - ? ($f= $this->body[$k]) - : (isset($this->body[$k= '@'.$parse->token->kind]) ? ($f= $this->body[$k]) : null) - ) { + } else if ($f= $this->body[$parse->token->value] ?? $this->body['@'.$parse->token->kind] ?? null) { $f($parse, $body, $annotations, $modifiers); $modifiers= []; $annotations= []; diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index f68f0040..2e243d08 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -62,7 +62,7 @@ public static function main(array $args) { break; } else { $in= $args[$i]; - $out= isset($args[$i + 1]) ? $args[$i + 1] : '-'; + $out= $args[$i + 1] ?? '-'; break; } } From aaae1b93395eee25c775c3b04d868951cdd785b3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 13:05:41 +0200 Subject: [PATCH 142/926] Group use statements --- .../lang/ast/CompilingClassloader.class.php | 10 +- src/main/php/lang/ast/Emitter.class.php | 3 +- src/main/php/lang/ast/Errors.class.php | 3 +- src/main/php/lang/ast/Language.class.php | 8 +- src/main/php/lang/ast/Result.class.php | 3 +- .../ast/emit/OmitConstModifiers.class.php | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 3 +- src/main/php/lang/ast/syntax/PHP.class.php | 114 +++++++++--------- .../ast/syntax/php/CompactMethods.class.php | 3 +- .../lang/ast/syntax/php/NullSafe.class.php | 8 +- .../php/lang/ast/syntax/php/Using.class.php | 10 +- .../php/xp/compiler/CompileRunner.class.php | 8 +- src/main/php/xp/compiler/FromFile.class.php | 3 +- src/main/php/xp/compiler/FromStream.class.php | 2 +- src/main/php/xp/compiler/ToFolder.class.php | 3 +- .../lang/ast/unittest/EmitterTest.class.php | 4 +- .../ast/unittest/emit/EmittingTest.class.php | 11 +- .../ast/unittest/emit/FileInput.class.php | 3 +- .../unittest/emit/FunctionTypesTest.class.php | 3 +- .../ast/unittest/emit/ImportTest.class.php | 2 +- .../unittest/emit/MultipleCatchTest.class.php | 3 +- .../ast/unittest/emit/ParameterTest.class.php | 7 +- .../emit/TransformationsTest.class.php | 6 +- .../unittest/emit/UnionTypesTest.class.php | 3 +- .../loader/CompilingClassLoaderTest.class.php | 10 +- .../ast/unittest/parse/BlocksTest.class.php | 4 +- .../ast/unittest/parse/ClosuresTest.class.php | 11 +- .../parse/CompactFunctionsTest.class.php | 6 +- .../unittest/parse/ConditionalTest.class.php | 7 +- .../unittest/parse/FunctionsTest.class.php | 13 +- .../ast/unittest/parse/InvokeTest.class.php | 5 +- .../ast/unittest/parse/LambdasTest.class.php | 9 +- .../ast/unittest/parse/LiteralsTest.class.php | 3 +- .../ast/unittest/parse/LoopsTest.class.php | 15 +-- .../ast/unittest/parse/MembersTest.class.php | 11 +- .../unittest/parse/NamespacesTest.class.php | 3 +- .../ast/unittest/parse/OperatorTest.class.php | 16 +-- .../ast/unittest/parse/ParseTest.class.php | 5 +- .../unittest/parse/StartTokensTest.class.php | 3 +- .../ast/unittest/parse/TypesTest.class.php | 6 +- .../unittest/parse/VariablesTest.class.php | 5 +- 41 files changed, 103 insertions(+), 254 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 9241f7f5..bd138de5 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,12 +1,6 @@ version, $value->version) : 1; } -} +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 031146d3..b29f2e18 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,7 +1,6 @@ emitOne($result, $const->expression); $result->out->write(';'); } -} +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 885597ff..64605642 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,6 @@ name) => $this->stream; } -} +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/ToFolder.class.php b/src/main/php/xp/compiler/ToFolder.class.php index 2a0e77ce..c1f11256 100755 --- a/src/main/php/xp/compiler/ToFolder.class.php +++ b/src/main/php/xp/compiler/ToFolder.class.php @@ -1,7 +1,6 @@ Date: Mon, 30 Sep 2019 13:07:06 +0200 Subject: [PATCH 143/926] Remove PHP 5 specific code --- .../php/lang/ast/CompilingClassloader.class.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index bd138de5..3417cbe3 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,7 +1,15 @@ getMessage(), $e); - } catch (\Exception $e) { - unset(\xp::$cl[$class]); - throw new ClassFormatException('Compiler error: '.$e->getMessage(), $e); } finally { \xp::$cll--; unset(Compiled::$source[$uri]); From 4982639e7f32763237b267bd6f70210ae21e72e5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 30 Sep 2019 13:13:28 +0200 Subject: [PATCH 144/926] Use anonymous classes where applicable --- src/test/php/lang/ast/unittest/EmitterTest.class.php | 6 +++--- .../php/lang/ast/unittest/emit/PrecedenceTest.class.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 685e5d15..a3f10c37 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -54,8 +54,8 @@ public function remove_unsets_empty_kind() { #[@test, @expect(IllegalStateException::class)] public function emit_node_without_kind() { - $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), newinstance(Node::class, [], [ - 'kind' => null - ])); + $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), new class() extends Node { + public $kind= null; + }); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index cf121551..7bcc6648 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -27,6 +27,6 @@ public function run() { } }' ); - $this->assertEquals('('.$t->getName().')', $t->newinstance()->run()); + $this->assertEquals('('.$t->getName().')', $t->newInstance()->run()); } } \ No newline at end of file From e9a5afe7e17e2448a34fd7b1df2947c0b330a4f8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 02:09:18 +0200 Subject: [PATCH 145/926] Add support for annotations in anonymous classes --- src/main/php/lang/ast/emit/PHP.class.php | 25 ++++++++++++- .../emit/AnonymousClassTest.class.php | 37 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8058e7de..49ebb5e6 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,10 @@ out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + if (null === $name) { + $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); + } else { + $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + } $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); $this->emitAnnotations($result, $annotations); $result->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); @@ -661,6 +668,8 @@ protected function emitNew($result, $new) { } protected function emitNewClass($result, $new) { + array_unshift($result->meta, []); + $result->out->write('new class('); $this->emitArguments($result, $new->arguments); $result->out->write(')'); @@ -668,11 +677,23 @@ protected function emitNewClass($result, $new) { $new->definition->parent && $result->out->write(' extends '.$new->definition->parent); $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); $result->out->write('{'); + + // Initialize meta data in constructor + if (isset($new->definition->body['__construct()'])) { + array_unshift($new->definition->body['__construct()']->body, new Code('self::__init()')); + } else { + $new->definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ + new Code('self::__init()') + ]); + } + foreach ($new->definition->body as $member) { $this->emitOne($result, $member); $result->out->write("\n"); } - $result->out->write('}'); + $result->out->write('static function __init() {'); + $this->emitMeta($result, null, [], null); + $result->out->write('}}'); } protected function emitInvoke($result, $invoke) { diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 4bd6a3bd..861820ae 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -50,4 +50,41 @@ public function run() { }'); $this->assertInstanceOf(Runnable::class, $r); } + + #[@test] + public function method_annotations() { + $r= $this->run('class { + public function run() { + return new class() { + + <> + public function fixture() { } + }; + } + + public function fixture() { } + }'); + + $this->assertEquals(['inside' => null], typeof($r)->getMethod('fixture')->getAnnotations()); + } + + #[@test] + public function method_annotations_with_constructor() { + $r= $this->run('class { + public function run() { + return new class("test") { + public $name; + + public function __construct($name) { + $this->name= $name; + } + + <> + public function fixture() { } + }; + } + }'); + $this->assertEquals(['test' => null], typeof($r)->getMethod('fixture')->getAnnotations()); + $this->assertEquals('test', $r->name); + } } \ No newline at end of file From 6f604cd3f3ada50e94a075a46c87421efe2e6a6b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 02:33:03 +0200 Subject: [PATCH 146/926] Also support annotations for anonymous classesin PHP 5.6 --- src/main/php/lang/ast/emit/PHP56.class.php | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 4e81c15e..dd374b70 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -1,5 +1,9 @@ meta, []); + $result->out->write('\\lang\\ClassLoader::defineType("class©anonymous'.md5(uniqid()).'", ["kind" => "class"'); $definition= $new->definition; $result->out->write(', "extends" => '.($definition->parent ? '[\''.$definition->parent.'\']' : 'null')); $result->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); $result->out->write(', "use" => []'); $result->out->write('], \'{'); - $result->out->write(str_replace('\'', '\\\'', $result->buffer(function($result) use($definition) { + $result->out->write(strtr($result->buffer(function($result) use($definition) { + + // Initialize meta data in constructor + if (isset($definition->body['__construct()'])) { + array_unshift($definition->body['__construct()']->body, new Code('self::__init()')); + } else { + $definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ + new Code('self::__init()') + ]); + } + foreach ($definition->body as $member) { $this->emitOne($result, $member); $result->out->write("\n"); } - }))); + + $result->out->write('static function __init() {'); + $this->emitMeta($result, null, [], null); + $result->out->write('}'); + + }), ['\'' => '\\\'', '\\' => '\\\\'])); $result->out->write('}\')->newInstance('); $this->emitArguments($result, $new->arguments); $result->out->write(')'); From 7beebd1ed2465f773ac579b41b554ede671b52e3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 02:47:04 +0200 Subject: [PATCH 147/926] Generate delegating constructor if base class exists See https://github.com/xp-framework/compiler/pull/73#discussion_r329841278 --- src/main/php/lang/ast/emit/PHP.class.php | 6 ++++++ src/main/php/lang/ast/emit/PHP56.class.php | 6 ++++++ .../emit/AnonymousClassTest.class.php | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 49ebb5e6..4c7ea7f0 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -4,6 +4,7 @@ use lang\ast\Emitter; use lang\ast\Node; use lang\ast\nodes\Method; +use lang\ast\nodes\Parameter; use lang\ast\nodes\Signature; abstract class PHP extends Emitter { @@ -681,6 +682,11 @@ protected function emitNewClass($result, $new) { // Initialize meta data in constructor if (isset($new->definition->body['__construct()'])) { array_unshift($new->definition->body['__construct()']->body, new Code('self::__init()')); + } else if ($new->definition->parent) { + $param= new Parameter('args', null, null, false, true, null, []); + $new->definition->body['__construct()']= new Method([], '__construct', new Signature([$param], null), [ + new Code('method_exists(parent::class, "__construct") && parent::__construct(...$args); self::__init()') + ]); } else { $new->definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ new Code('self::__init()') diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index dd374b70..1a5884f9 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -2,6 +2,7 @@ use lang\ast\Code; use lang\ast\nodes\Method; +use lang\ast\nodes\Parameter; use lang\ast\nodes\Signature; /** @@ -212,6 +213,11 @@ protected function emitNewClass($result, $new) { // Initialize meta data in constructor if (isset($definition->body['__construct()'])) { array_unshift($definition->body['__construct()']->body, new Code('self::__init()')); + } else if ($definition->parent) { + $param= new Parameter('args', null, null, false, true, null, []); + $definition->body['__construct()']= new Method([], '__construct', new Signature([$param], null), [ + new Code('method_exists(parent::class, "__construct") && parent::__construct(...$args); self::__init()') + ]); } else { $definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ new Code('self::__init()') diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 861820ae..2c90d9a2 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -87,4 +87,25 @@ public function fixture() { } $this->assertEquals(['test' => null], typeof($r)->getMethod('fixture')->getAnnotations()); $this->assertEquals('test', $r->name); } + + #[@test] + public function method_annotations_with_inherited_constructor() { + $r= $this->run('class { + public $name; + + public function __construct($name= null) { + $this->name= $name; + } + + public function run() { + return new class("test") extends { + + <> + public function fixture() { } + }; + } + }'); + $this->assertEquals(['test' => null], typeof($r)->getMethod('fixture')->getAnnotations()); + $this->assertEquals('test', $r->name); + } } \ No newline at end of file From 4c631f85abe05b091cb63407a89cb244afaed3c5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 02:59:27 +0200 Subject: [PATCH 148/926] Do not rewrite constructors, instead use fluent interface See https://github.com/xp-framework/compiler/pull/73#issuecomment-536811680 --- src/main/php/lang/ast/emit/PHP.class.php | 24 +++----------------- src/main/php/lang/ast/emit/PHP56.class.php | 26 +++------------------- 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 4c7ea7f0..66322694 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,11 +1,7 @@ meta, []); - $result->out->write('new class('); + $result->out->write('(new class('); $this->emitArguments($result, $new->arguments); $result->out->write(')'); @@ -679,27 +675,13 @@ protected function emitNewClass($result, $new) { $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); $result->out->write('{'); - // Initialize meta data in constructor - if (isset($new->definition->body['__construct()'])) { - array_unshift($new->definition->body['__construct()']->body, new Code('self::__init()')); - } else if ($new->definition->parent) { - $param= new Parameter('args', null, null, false, true, null, []); - $new->definition->body['__construct()']= new Method([], '__construct', new Signature([$param], null), [ - new Code('method_exists(parent::class, "__construct") && parent::__construct(...$args); self::__init()') - ]); - } else { - $new->definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ - new Code('self::__init()') - ]); - } - foreach ($new->definition->body as $member) { $this->emitOne($result, $member); $result->out->write("\n"); } - $result->out->write('static function __init() {'); + $result->out->write('function __new() {'); $this->emitMeta($result, null, [], null); - $result->out->write('}}'); + $result->out->write('return $this; }})->__new()'); } protected function emitInvoke($result, $invoke) { diff --git a/src/main/php/lang/ast/emit/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php index 1a5884f9..31fa11fe 100755 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ b/src/main/php/lang/ast/emit/PHP56.class.php @@ -1,10 +1,5 @@ out->write(', "use" => []'); $result->out->write('], \'{'); $result->out->write(strtr($result->buffer(function($result) use($definition) { - - // Initialize meta data in constructor - if (isset($definition->body['__construct()'])) { - array_unshift($definition->body['__construct()']->body, new Code('self::__init()')); - } else if ($definition->parent) { - $param= new Parameter('args', null, null, false, true, null, []); - $definition->body['__construct()']= new Method([], '__construct', new Signature([$param], null), [ - new Code('method_exists(parent::class, "__construct") && parent::__construct(...$args); self::__init()') - ]); - } else { - $definition->body['__construct()']= new Method([], '__construct', new Signature([], null), [ - new Code('self::__init()') - ]); - } - foreach ($definition->body as $member) { $this->emitOne($result, $member); $result->out->write("\n"); } - $result->out->write('static function __init() {'); + $result->out->write('function __new() {'); $this->emitMeta($result, null, [], null); - $result->out->write('}'); + $result->out->write('return $this; }'); }), ['\'' => '\\\'', '\\' => '\\\\'])); $result->out->write('}\')->newInstance('); $this->emitArguments($result, $new->arguments); - $result->out->write(')'); + $result->out->write(')->__new()'); } protected function emitFrom($result, $from) { From f192893a0603909f34a9437e46503346f7a5d317 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 03:01:49 +0200 Subject: [PATCH 149/926] Remove superfluous test code --- .../emit/AnonymousClassTest.class.php | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 2c90d9a2..3b2fea3c 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -61,51 +61,8 @@ public function run() { public function fixture() { } }; } - - public function fixture() { } }'); $this->assertEquals(['inside' => null], typeof($r)->getMethod('fixture')->getAnnotations()); } - - #[@test] - public function method_annotations_with_constructor() { - $r= $this->run('class { - public function run() { - return new class("test") { - public $name; - - public function __construct($name) { - $this->name= $name; - } - - <> - public function fixture() { } - }; - } - }'); - $this->assertEquals(['test' => null], typeof($r)->getMethod('fixture')->getAnnotations()); - $this->assertEquals('test', $r->name); - } - - #[@test] - public function method_annotations_with_inherited_constructor() { - $r= $this->run('class { - public $name; - - public function __construct($name= null) { - $this->name= $name; - } - - public function run() { - return new class("test") extends { - - <> - public function fixture() { } - }; - } - }'); - $this->assertEquals(['test' => null], typeof($r)->getMethod('fixture')->getAnnotations()); - $this->assertEquals('test', $r->name); - } } \ No newline at end of file From 797bbc97fbc88cf02fc14af414b8adb22fc8b327 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 1 Oct 2019 13:08:07 +0200 Subject: [PATCH 150/926] Prepare 4.1-RELEASE --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 4c06c8df..b21df668 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 4.1.0 / 2019-10-01 + +* Merged PR #73: Add support for annotations in anonymous classes + (@thekid) + ## 4.0.0 / 2019-09-09 * Merged PR #69: Remove support for Hack arrow functions - @thekid From f03306c69b185a2fc5c54941169581c44a97e3b2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 2 Oct 2019 14:49:24 +0200 Subject: [PATCH 151/926] Verify arrow functions work inside annotations See xp-framework/core#227 --- .../php/lang/ast/unittest/emit/AnnotationsTest.class.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 1c032572..fa013476 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -44,6 +44,13 @@ public function closure_value() { $this->assertEquals('test', $f('test')); } + #[@test] + public function arrow_function_value() { + $t= $this->type('< $arg)>> class { }'); + $f= $t->getAnnotation('verify'); + $this->assertEquals('test', $f('test')); + } + #[@test] public function has_access_to_class() { $t= $this->type('<> class { const SUCCESS = true; }'); From 20ab402389f2eab49baba2b3de56fd2967e8abf0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 12:34:30 +0200 Subject: [PATCH 152/926] Add "ast" subcommand to display the abstract syntax tree --- bin/xp.xp-framework.compiler.ast | 2 + composer.json | 2 +- src/main/php/xp/compiler/AstRunner.class.php | 102 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100755 bin/xp.xp-framework.compiler.ast create mode 100755 src/main/php/xp/compiler/AstRunner.class.php diff --git a/bin/xp.xp-framework.compiler.ast b/bin/xp.xp-framework.compiler.ast new file mode 100755 index 00000000..5837670f --- /dev/null +++ b/bin/xp.xp-framework.compiler.ast @@ -0,0 +1,2 @@ +#!/usr/bin/env php +This must be run from within an XP runner \ No newline at end of file diff --git a/composer.json b/composer.json index 371a6cde..c794beef 100755 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "require-dev" : { "xp-framework/unittest": "^9.3" }, - "bin": ["bin/xp.xp-framework.compiler.compile"], + "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { "files" : ["src/main/php/autoload.php"] } diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php new file mode 100755 index 00000000..eddede3e --- /dev/null +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -0,0 +1,102 @@ + $prop) { + if ('kind' === $name || 'line' === $name) continue; + $p[$name]= $prop; + } + + $s= -1 === $value->line + ? sprintf("\033[36m%s\033[0m(\033[34m:%s\033[0m", nameof($value), $value->kind) + : sprintf("\033[36m%s\033[0m(\033[32m#%03d\033[0m, \033[34m:%s\033[0m", nameof($value), $value->line, $value->kind) + ; + switch (sizeof($p)) { + case 0: return $s.')'; + case 1: return $s.', '.key($p).'= '.self::stringOf(current($p), $indent).')'; + default: + $s.= ")@{\n"; + $i= $indent.' '; + foreach ($p as $name => $prop) { + $s.= $i.$name.' => '.self::stringOf($prop, $i)."\n"; + } + $s.= $indent.'}'; + return $s; + } + } else if (is_array($value)) { + if (empty($value)) return '[]'; + $s= "[\n"; + $i= $indent.' '; + if (0 === key($value)) { + foreach ($value as $val) { + $s.= $i.self::stringOf($val, $i)."\n"; + } + } else { + foreach ($value as $key => $val) { + $s.= $i.$key.' => '.self::stringOf($val, $i)."\n"; + } + } + return $s.$indent.']'; + } else if (is_string($value)) { + return "\033[34m\"".$value."\"\033[0m"; + } else { + return Objects::stringOf($value); + } + } + + /** @return int */ + public static function main(array $args) { + if (empty($args)) { + Console::writeLine('Usage: xp ast [file]'); + } + + $lang= Language::named('PHP'); + $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + foreach ($lang->extensions() as $extension) { + $extension->setup($lang, $emit); + } + + $input= Input::newInstance($args[0]); + $errors= 0; + foreach ($input as $path => $in) { + $file= $path->toString('/'); + Console::writeLinef("\033[1m══ %s ══%s\033[0m", $file, str_repeat('═', 72 - 6 - strlen($file))); + + try { + $parse= new Parse($lang, new Tokens(new StreamTokenizer($in)), $file); + foreach ($parse->execute() as $node) { + Console::writeLine(self::stringOf($node)); + } + } catch (Errors $e) { + Console::$err->writeLinef("\033[41;1;37m! %s: %s\033[0m", $file, $e->diagnostics(' ')); + $errors++; + } + } + return $errors ? 1 : 0; + } +} \ No newline at end of file From 84e29187d4adab38047dbbef712bf30699d1dbc1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 12:46:25 +0200 Subject: [PATCH 153/926] Fix error message when parsing offsets inside curly braces --- src/main/php/lang/ast/syntax/PHP.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 5003adc2..2afea6bf 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -178,7 +178,7 @@ public function __construct() { $this->infix('{', 80, function($parse, $token, $left) { $expr= $this->expression($parse, 0); - $parse->expecting('}', 'dynamic member'); + $parse->expecting('}', 'offset'); return new OffsetExpression($left, $expr, $token->line); }); @@ -425,7 +425,6 @@ public function __construct() { return new Variable($token->value, $token->line); }); - $this->prefix('(literal)', 0, function($parse, $token) { return new Literal($token->value, $token->line); }); From 6368653f63f4bf11f4798a0e9a85a95ebde5ddcd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 13:03:17 +0200 Subject: [PATCH 154/926] Announce new "ast" XP subcommand --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b21df668..66d55c27 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #75: Add "ast" subcommand to display the abstract syntax tree + (@thekid) + ## 4.1.0 / 2019-10-01 * Merged PR #73: Add support for annotations in anonymous classes From 2d3372ac9a641f2f6a6f0a73414187297f534a35 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 13:19:31 +0200 Subject: [PATCH 155/926] Add test verifying compile errors are raised for ==> See issue #74 --- .../php/lang/ast/unittest/emit/LambdasTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 13033cd0..ea8b3d05 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -1,5 +1,6 @@ assertEquals(2, $r()); } + + #[@test, @expect(Errors::class)] + public function no_longer_supports_hacklang_variant() { + $this->run('class { + public function run() { + $func= ($arg) ==> { return 1; }; + } + }'); + } } \ No newline at end of file From 0f6487300d1ffc6b69ef26f8a2f75233e6aa24ac Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 13:29:25 +0200 Subject: [PATCH 156/926] Fix issue #74, no longer shadow compile errors --- ChangeLog.md | 2 ++ src/main/php/lang/ast/emit/PHP.class.php | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 66d55c27..aa99da8b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed issue #74: No longer shadow compiler errors in certain cases + (@thekid) * Merged PR #75: Add "ast" subcommand to display the abstract syntax tree (@thekid) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 66322694..1f15eb94 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -85,6 +85,10 @@ protected function emitName($result, $name) { $result->out->write($name); } + protected function emitOperator($result, $operator) { + $result->out->write($operator->value); + } + protected function emitEcho($result, $echo) { $result->out->write('echo '); $s= sizeof($echo->expressions) - 1; From 207d5b0ddfc1feec330fd584f746ffb6b89496fc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 13:29:35 +0200 Subject: [PATCH 157/926] Prepare 4.2.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index aa99da8b..ffb72e5a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 4.2.0 / 2019-10-04 + * Fixed issue #74: No longer shadow compiler errors in certain cases (@thekid) * Merged PR #75: Add "ast" subcommand to display the abstract syntax tree From 793f0e39ecf2dfef6f95b36052f14083550c8aa7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 13:38:30 +0200 Subject: [PATCH 158/926] Prevent "Object of class lang\ast\Token could not be converted to string" --- src/main/php/lang/ast/emit/PHP.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1f15eb94..b10e011f 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -73,6 +73,10 @@ protected function emitAnnotation($result, $annotations) { // NOOP } + protected function emitOperator($result, $operator) { + // NOOP + } + protected function emitCode($result, $code) { $result->out->write($code->value); } @@ -85,10 +89,6 @@ protected function emitName($result, $name) { $result->out->write($name); } - protected function emitOperator($result, $operator) { - $result->out->write($operator->value); - } - protected function emitEcho($result, $echo) { $result->out->write('echo '); $s= sizeof($echo->expressions) - 1; From 24b9a6c4782b971c283fa94032f2a2e00ac9a7be Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2019 15:41:29 +0200 Subject: [PATCH 159/926] Prevent "Object of class lang\ast\Token could not be converted to string" --- src/main/php/lang/ast/emit/PHP.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b10e011f..3fccdb93 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -77,6 +77,10 @@ protected function emitOperator($result, $operator) { // NOOP } + protected function emitName($result, $name) { + // NOOP + } + protected function emitCode($result, $code) { $result->out->write($code->value); } @@ -85,10 +89,6 @@ protected function emitLiteral($result, $literal) { $result->out->write($literal->expression); } - protected function emitName($result, $name) { - $result->out->write($name); - } - protected function emitEcho($result, $echo) { $result->out->write('echo '); $s= sizeof($echo->expressions) - 1; From 931871eb95227096e52caeada3ad71975578d286 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Oct 2019 18:40:22 +0200 Subject: [PATCH 160/926] Make abstract and final "extensible" See https://github.com/xp-lang/xp-enums/pull/1#discussion_r331752640 --- src/main/php/lang/ast/syntax/PHP.class.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 2afea6bf..d384c094 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -681,17 +681,15 @@ public function __construct() { }); $this->stmt('abstract', function($parse, $token) { - $parse->forward(); - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - return $this->clazz($parse, $type, ['abstract']); + $type= $this->statement($parse); + $type->modifiers[]= 'abstract'; + return $type; }); $this->stmt('final', function($parse, $token) { - $parse->forward(); - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - return $this->clazz($parse, $type, ['final']); + $type= $this->statement($parse); + $type->modifiers[]= 'final'; + return $type; }); $this->stmt('<<', function($parse, $token) { From b54d7bb26765fc07fda5488cef4c381a695d0e17 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Oct 2019 18:41:19 +0200 Subject: [PATCH 161/926] Prepare 4.2.1-RELEASE --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index ffb72e5a..2a3c4c52 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 4.2.1 / 2019-10-05 + +* Fixed parser to allow "extending" final and abstract types - @thekid + ## 4.2.0 / 2019-10-04 * Fixed issue #74: No longer shadow compiler errors in certain cases From ed41def305b87d111f9dc8a6189a3b3345c1685e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 19 Oct 2019 16:00:32 +0200 Subject: [PATCH 162/926] QA: Fix test naming --- src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 21a219c5..4ee28f1b 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -100,7 +100,7 @@ public function run($user) { } #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_ternary() { + public function throw_expression_with_short_ternary() { $t= $this->type('class { public function run($user) { return $user ?: throw new \\lang\\IllegalArgumentException("test"); @@ -110,7 +110,7 @@ public function run($user) { } #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_short_ternary() { + public function throw_expression_with_normal_ternary() { $t= $this->type('class { public function run($user) { return $user ? new User($user) : throw new \\lang\\IllegalArgumentException("test"); From d6ee85a2196b381a16e9acf6d966585b018d19a6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 14:34:55 +0100 Subject: [PATCH 163/926] Add support for #-style comments --- src/main/php/lang/ast/Tokens.class.php | 3 + .../ast/unittest/parse/CommentTest.class.php | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/parse/CommentTest.class.php diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index 634b8d70..c240d8b7 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -99,6 +99,9 @@ public function getIterator() { continue; } $this->source->pushBack($next); + } else if ('#' === $token) { + $this->source->nextToken("\r\n"); + continue; } if (isset(self::$operators[$token])) { diff --git a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php new file mode 100755 index 00000000..4060426b --- /dev/null +++ b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php @@ -0,0 +1,58 @@ +assertParsed([new Literal('"test"', 3)], ' + // This is a comment + "test"; + '); + } + + #[@test] + public function two_oneline_double_slash() { + $this->assertParsed([new Literal('"test"', 4)], ' + // This is a comment + // This is another + "test"; + '); + } + + #[@test] + public function oneline_hashtag() { + $this->assertParsed([new Literal('"test"', 3)], ' + # This is a comment + "test"; + '); + } + + #[@test] + public function two_oneline_hashtags() { + $this->assertParsed([new Literal('"test"', 4)], ' + # This is a comment + # This is another + "test"; + '); + } + + #[@test] + public function oneline_slash_asterisk() { + $this->assertParsed([new Literal('"test"', 3)], ' + /* This is a comment */ + "test"; + '); + } + + #[@test] + public function multiline_slash_asterisk() { + $this->assertParsed([new Literal('"test"', 5)], ' + /* This is a comment + * spanning multiple lines. + */ + "test"; + '); + } +} \ No newline at end of file From a85a0c06777460f6f88c63becd661c9ebc3290ba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 14:39:16 +0100 Subject: [PATCH 164/926] Test additional variants of comments --- .../ast/unittest/parse/CommentTest.class.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php index 4060426b..e4afae46 100755 --- a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php @@ -12,6 +12,13 @@ public function oneline_double_slash() { '); } + #[@test] + public function oneline_double_slash_at_end() { + $this->assertParsed([new Literal('"test"', 2)], ' + "test"; // This is a comment + '); + } + #[@test] public function two_oneline_double_slash() { $this->assertParsed([new Literal('"test"', 4)], ' @@ -29,6 +36,13 @@ public function oneline_hashtag() { '); } + #[@test] + public function oneline_hashtag_at_end() { + $this->assertParsed([new Literal('"test"', 2)], ' + "test"; # This is a comment + '); + } + #[@test] public function two_oneline_hashtags() { $this->assertParsed([new Literal('"test"', 4)], ' @@ -46,6 +60,20 @@ public function oneline_slash_asterisk() { '); } + #[@test] + public function oneline_slash_asterisk_at_end() { + $this->assertParsed([new Literal('"test"', 2)], ' + "test"; /* This is a comment */ + '); + } + + #[@test] + public function oneline_slash_asterisk_inbetween() { + $this->assertParsed([new Literal('"before"', 2), new Literal('"after"', 2)], ' + "before"; /* This is a comment */ "after"; + '); + } + #[@test] public function multiline_slash_asterisk() { $this->assertParsed([new Literal('"test"', 5)], ' From e51fe56c0bee8268d1dbdf98ebad9c499379f190 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 15:07:18 +0100 Subject: [PATCH 165/926] Raise an error if we encounter XP-style annotations Easiest way for the beginning See https://github.com/xp-framework/compiler/pull/77#issuecomment-554747248 --- src/main/php/lang/ast/Tokens.class.php | 5 ++++- .../php/lang/ast/unittest/emit/AnnotationsTest.class.php | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index c240d8b7..ec65338e 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -100,7 +100,10 @@ public function getIterator() { } $this->source->pushBack($next); } else if ('#' === $token) { - $this->source->nextToken("\r\n"); + $next= $this->source->nextToken("\r\n"); + if (0 === strncmp($next, '[@', 2)) { + throw new FormatException('XP style annotations are not supported at line '.$line); + } continue; } diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index fa013476..6b0603f1 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,5 +1,6 @@ type('class { <> public function fixture() { } }'); $this->assertEquals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } + + #[@test, @expect(class= FormatException::class, withMessage= 'XP style annotations are not supported at line 2')] + public function xpstyle_annotations_are_not_supported() { + $this->type(' + #[@test] + class { }' + ); + } } \ No newline at end of file From 9086433fd75a535579d2e733fbcf7cf02926d6b4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 16:29:54 +0100 Subject: [PATCH 166/926] Extract annotation parsing into dedicated method --- src/main/php/lang/ast/syntax/PHP.class.php | 115 ++++++------------ .../ast/unittest/parse/ErrorsTest.class.php | 2 +- 2 files changed, 41 insertions(+), 76 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index d384c094..962b219d 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -693,32 +693,11 @@ public function __construct() { }); $this->stmt('<<', function($parse, $token) { - $values= []; - do { - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->forward(); - $values[$name]= $parse->scope->annotations[$name]= $this->expression($parse, 0); - $parse->expecting(')', 'annotations'); - } else { - $values[$name]= $parse->scope->annotations[$name]= null; - } - - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', 'annotation'); - break; - } - } while (null !== $parse->token->value); + $parse->queue[]= $parse->token; + $parse->token= $token; + $parse->scope->annotations= $this->annotations($parse, 'annotations'); - $parse->forward(); - return new Annotations($values, $token->line); + return new Annotations($parse->scope->annotations, $token->line); }); $this->stmt('class', function($parse, $token) { @@ -995,37 +974,45 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { $parse->expecting(';', 'field declaration'); } + private function annotations($parse, $context) { + if ('<<' === $parse->token->value) { + $annotations= []; + do { + $parse->forward(); + + $name= $parse->token->value; + $parse->forward(); + + if ('(' === $parse->token->value) { + $parse->expecting('(', $context); + $annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', $context); + } else { + $annotations[$name]= null; + } + + if (',' === $parse->token->value) { + continue; + } else if ('>>' === $parse->token->value) { + break; + } else { + $parse->expecting(', or >>', $context); + } + } while (null !== $parse->token->value); + + $parse->expecting('>>', $context); + return $annotations; + } else { + return []; + } + } + private function parameters($parse) { static $promotion= ['private' => true, 'protected' => true, 'public' => true]; $parameters= []; - $annotations= []; while (')' !== $parse->token->value) { - if ('<<' === $parse->token->value) { - do { - $parse->forward(); - - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->expecting('(', 'parameters'); - $annotations[$name]= $this->expression($parse, 0); - $parse->expecting(')', 'parameters'); - } else { - $annotations[$name]= null; - } - - if (',' === $parse->token->value) { - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', 'parameter annotation'); - } - } while (null !== $parse->token->value); - $parse->expecting('>>', 'parameter annotation'); - } + $annotations= $this->annotations($parse, 'parameter annotation'); if ('name' === $parse->token->kind && isset($promotion[$parse->token->value])) { $promote= $parse->token->value; @@ -1102,30 +1089,8 @@ public function typeBody($parse) { $f($parse, $body, $annotations, $modifiers); $modifiers= []; $annotations= []; - } else if ('<<' === $parse->token->symbol->id) { - do { - $parse->forward(); - - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->forward(); - $annotations[$name]= $this->expression($parse, 0); - $parse->expecting(')', 'annotations'); - } else { - $annotations[$name]= null; - } - - if (',' === $parse->token->value) { - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', 'annotations'); - } - } while (null !== $parse->token->value); - $parse->forward(); + } else if ('<<' === $parse->token->value) { + $annotations= $this->annotations($parse, 'member annotations'); } else if ($type= $this->type($parse)) { $this->properties($parse, $body, $annotations, $modifiers, $type); $modifiers= []; diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index 0dfd2790..d58f89dd 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -71,7 +71,7 @@ public function missing_comma_in_interface_parents() { #[@test] public function unclosed_annotation() { $this->assertError( - 'Expected ", or >>", have "(end)" in annotation', + 'Expected ", or >>", have "(end)" in annotations', $this->parse('< Date: Sun, 17 Nov 2019 16:32:24 +0100 Subject: [PATCH 167/926] Simplify string parsing --- src/main/php/lang/ast/Tokens.class.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index ec65338e..80ad5781 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -55,14 +55,12 @@ public function getIterator() { throw new FormatException('Unclosed string literal starting at line '.$line); } else if ('\\' === $t) { $string.= $t.$this->source->nextToken($end); - } else if ($token === $t) { - break; } else { $string.= $t; } - } while (true); + } while ($token !== $t); - yield 'string' => [$string.$token, $line]; + yield 'string' => [$string, $line]; $line+= substr_count($string, "\n"); } else if (0 === strcspn($token, " \r\n\t")) { $line+= substr_count($token, "\n"); From bdc7e889dd28456daf4a5dcd82916b3ce9b0fdb5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 17:48:32 +0100 Subject: [PATCH 168/926] Add support for XP style annotations TODO: `@$param: annotation` is not yet supported --- src/main/php/lang/ast/Tokens.class.php | 17 +++- src/main/php/lang/ast/syntax/PHP.class.php | 93 +++++++++++++------ .../unittest/emit/AnnotationsTest.class.php | 50 +++++++++- 3 files changed, 126 insertions(+), 34 deletions(-) diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index 80ad5781..40596900 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -98,9 +98,20 @@ public function getIterator() { } $this->source->pushBack($next); } else if ('#' === $token) { - $next= $this->source->nextToken("\r\n"); - if (0 === strncmp($next, '[@', 2)) { - throw new FormatException('XP style annotations are not supported at line '.$line); + $comment= $this->source->nextToken("\r\n").$this->source->nextToken("\r\n"); + $next= '#'; + do { + $s= strspn($next, ' '); + if ('#' !== $next[$s]) break; + $line++; + $comment.= substr($next, $s + 1); + $next= $this->source->nextToken("\r\n").$this->source->nextToken("\r\n"); + } while ($this->source->hasMoreTokens()); + if (0 === strncmp($comment, '[@', 2)) { + $this->source->pushBack(substr($comment, 1).$next); + yield 'operator' => ['#[', $line]; + } else { + $this->source->pushBack($next); } continue; } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 962b219d..f6f6ba39 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -693,13 +693,17 @@ public function __construct() { }); $this->stmt('<<', function($parse, $token) { - $parse->queue[]= $parse->token; - $parse->token= $token; $parse->scope->annotations= $this->annotations($parse, 'annotations'); return new Annotations($parse->scope->annotations, $token->line); }); + $this->stmt('#[', function($parse, $token) { + $parse->scope->annotations= $this->meta($parse, 'annotations'); + + return new Annotations($parse->scope->annotations, $token->line); + }); + $this->stmt('class', function($parse, $token) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); @@ -975,36 +979,60 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { } private function annotations($parse, $context) { - if ('<<' === $parse->token->value) { - $annotations= []; - do { - $parse->forward(); + $annotations= []; + do { + $name= $parse->token->value; + $parse->forward(); - $name= $parse->token->value; + if ('(' === $parse->token->value) { + $parse->expecting('(', $context); + $annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', $context); + } else { + $annotations[$name]= null; + } + + if (',' === $parse->token->value) { $parse->forward(); + continue; + } else if ('>>' === $parse->token->value) { + break; + } else { + $parse->expecting(', or >>', $context); + } + } while (null !== $parse->token->value); - if ('(' === $parse->token->value) { - $parse->expecting('(', $context); - $annotations[$name]= $this->expression($parse, 0); - $parse->expecting(')', $context); - } else { - $annotations[$name]= null; - } + $parse->expecting('>>', $context); + return $annotations; + } - if (',' === $parse->token->value) { - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', $context); - } - } while (null !== $parse->token->value); + private function meta($parse, $context) { + $annotations= []; + do { + $parse->expecting('@', $context); + $name= $parse->token->value; + $parse->forward(); - $parse->expecting('>>', $context); - return $annotations; - } else { - return []; - } + if ('(' === $parse->token->value) { + $parse->expecting('(', $context); + $annotations[$name]= $this->expression($parse, 0); + $parse->expecting(')', $context); + } else { + $annotations[$name]= null; + } + + if (',' === $parse->token->value) { + $parse->forward(); + continue; + } else if (']' === $parse->token->value) { + break; + } else { + $parse->expecting(', or ]', $context); + } + } while (null !== $parse->token->value); + + $parse->expecting(']', $context); + return $annotations; } private function parameters($parse) { @@ -1012,7 +1040,12 @@ private function parameters($parse) { $parameters= []; while (')' !== $parse->token->value) { - $annotations= $this->annotations($parse, 'parameter annotation'); + if ('<<' === $parse->token->value) { + $parse->forward(); + $annotations= $this->annotations($parse, 'parameter annotation'); + } else { + $annotations= []; + } if ('name' === $parse->token->kind && isset($promotion[$parse->token->value])) { $promote= $parse->token->value; @@ -1090,7 +1123,11 @@ public function typeBody($parse) { $modifiers= []; $annotations= []; } else if ('<<' === $parse->token->value) { + $parse->forward(); $annotations= $this->annotations($parse, 'member annotations'); + } else if ('#[' === $parse->token->value) { + $parse->forward(); + $annotations= $this->meta($parse, 'member annotations'); } else if ($type= $this->type($parse)) { $this->properties($parse, $body, $annotations, $modifiers, $type); $modifiers= []; diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 6b0603f1..437a565e 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -98,11 +98,55 @@ public function multiple_member_annotations() { $this->assertEquals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } - #[@test, @expect(class= FormatException::class, withMessage= 'XP style annotations are not supported at line 2')] - public function xpstyle_annotations_are_not_supported() { - $this->type(' + #[@test] + public function xp_type_annotation() { + $t= $this->type(' #[@test] class { }' ); + $this->assertEquals(['test' => null], $t->getAnnotations()); + } + + #[@test] + public function xp_type_annotations() { + $t= $this->type(' + #[@resource("/"), @authenticated] + class { }' + ); + $this->assertEquals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + } + + #[@test] + public function xp_type_multiline() { + $t= $this->type(' + #[@verify(function($arg) { + # return $arg; + #})] + class { }' + ); + $f= $t->getAnnotation('verify'); + $this->assertEquals('test', $f('test')); + } + + #[@test] + public function xp_method_annotations() { + $t= $this->type(' + class { + + #[@test] + public function succeeds() { } + + #[@test, @expect(\lang\IllegalArgumentException::class)] + public function fails() { } + + #[@test, @values([ + # [1, 2, 3], + #])] + public function cases() { } + }' + ); + $this->assertEquals(['test' => null], $t->getMethod('succeeds')->getAnnotations()); + $this->assertEquals(['test' => null, 'expect' => IllegalArgumentException::class], $t->getMethod('fails')->getAnnotations()); + $this->assertEquals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); } } \ No newline at end of file From 815f595832086e106fe17598599e2cec9d98fca9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 18:16:36 +0100 Subject: [PATCH 169/926] Support XP parameter annotations --- src/main/php/lang/ast/emit/PHP.class.php | 10 +++--- src/main/php/lang/ast/syntax/PHP.class.php | 31 +++++++++++++------ .../unittest/emit/AnnotationsTest.class.php | 13 ++++++++ .../ast/unittest/parse/MembersTest.class.php | 10 ++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3fccdb93..002f4fe2 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -377,7 +377,7 @@ protected function emitConst($result, $const) { protected function emitProperty($result, $property) { $result->meta[0][self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', - DETAIL_ANNOTATIONS => $property->annotations ? $property->annotations : [], + DETAIL_ANNOTATIONS => isset($property->annotations[DETAIL_ANNOTATIONS]) ? $property->annotations[DETAIL_ANNOTATIONS] : [], DETAIL_COMMENT => $property->comment, DETAIL_TARGET_ANNO => [], DETAIL_ARGUMENTS => [] @@ -396,9 +396,9 @@ protected function emitMethod($result, $method) { $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', - DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], + DETAIL_ANNOTATIONS => isset($method->annotations[DETAIL_ANNOTATIONS]) ? $method->annotations[DETAIL_ANNOTATIONS] : [], DETAIL_COMMENT => $method->comment, - DETAIL_TARGET_ANNO => [], + DETAIL_TARGET_ANNO => isset($method->annotations[DETAIL_TARGET_ANNO]) ? $method->annotations[DETAIL_TARGET_ANNO] : [], DETAIL_ARGUMENTS => [] ]; @@ -415,7 +415,9 @@ protected function emitMethod($result, $method) { DETAIL_ARGUMENTS => [] ]; } - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; + foreach ($param->annotations as $name => $value) { + $meta[DETAIL_TARGET_ANNO][$param->name][$name]= $value; + } $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } $result->out->write($declare); diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index f6f6ba39..b75cc355 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -699,7 +699,7 @@ public function __construct() { }); $this->stmt('#[', function($parse, $token) { - $parse->scope->annotations= $this->meta($parse, 'annotations'); + $parse->scope->annotations= $this->meta($parse, 'annotations')[DETAIL_ANNOTATIONS]; return new Annotations($parse->scope->annotations, $token->line); }); @@ -1007,18 +1007,28 @@ private function annotations($parse, $context) { } private function meta($parse, $context) { - $annotations= []; + $annotations= [DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []]; do { $parse->expecting('@', $context); + + if ('variable' === $parse->token->kind) { + $param= $parse->token->value; + $parse->forward(); + $parse->expecting(':', $context); + $a= &$annotations[DETAIL_TARGET_ANNO][$param]; + } else { + $a= &$annotations[DETAIL_ANNOTATIONS]; + } + $name= $parse->token->value; $parse->forward(); if ('(' === $parse->token->value) { $parse->expecting('(', $context); - $annotations[$name]= $this->expression($parse, 0); + $a[$name]= $this->expression($parse, 0); $parse->expecting(')', $context); } else { - $annotations[$name]= null; + $a[$name]= null; } if (',' === $parse->token->value) { @@ -1110,7 +1120,7 @@ public function typeBody($parse) { $body= []; $modifiers= []; - $annotations= []; + $meta= []; while ('}' !== $parse->token->value) { if (isset($modifier[$parse->token->value])) { $modifiers[]= $parse->token->value; @@ -1119,18 +1129,19 @@ public function typeBody($parse) { ? ($f= $this->body[$k]) : (isset($this->body[$k= '@'.$parse->token->kind]) ? ($f= $this->body[$k]) : null) ) { - $f($parse, $body, $annotations, $modifiers); + $f($parse, $body, $meta, $modifiers); $modifiers= []; - $annotations= []; + $meta= []; } else if ('<<' === $parse->token->value) { $parse->forward(); - $annotations= $this->annotations($parse, 'member annotations'); + $meta= [DETAIL_ANNOTATIONS => $this->annotations($parse, 'member annotations'), DETAIL_TARGET_ANNO => []]; } else if ('#[' === $parse->token->value) { $parse->forward(); - $annotations= $this->meta($parse, 'member annotations'); + $meta= $this->meta($parse, 'member annotations'); } else if ($type= $this->type($parse)) { - $this->properties($parse, $body, $annotations, $modifiers, $type); + $this->properties($parse, $body, $meta, $modifiers, $type); $modifiers= []; + $meta= []; } else { $parse->raise(sprintf( 'Expected a type, modifier, property, annotation, method or "}", have "%s"', diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 437a565e..cf23610d 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -149,4 +149,17 @@ public function cases() { } $this->assertEquals(['test' => null, 'expect' => IllegalArgumentException::class], $t->getMethod('fails')->getAnnotations()); $this->assertEquals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); } + + #[@test] + public function xp_param_annotation() { + $t= $this->type(' + class { + + #[@test, @$arg: inject("conn")] + public function fixture($arg) { } + }' + ); + $this->assertEquals(['test' => null], $t->getMethod('fixture')->getAnnotations()); + $this->assertEquals(['inject' => 'conn'], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index 138d257d..e01e0548 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -106,7 +106,10 @@ public function method_with_return_type() { #[@test] public function method_with_annotation() { - $annotations= ['test' => null]; + $annotations= [ + DETAIL_ANNOTATIONS => ['test' => null], + DETAIL_TARGET_ANNO => [] + ]; $body= [ 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) ]; @@ -118,7 +121,10 @@ public function method_with_annotation() { #[@test] public function method_with_annotations() { - $annotations= ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)]; + $annotations= [ + DETAIL_ANNOTATIONS => ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)], + DETAIL_TARGET_ANNO => [] + ]; $body= [ 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) ]; From 3da837cf60e48b2a21e5d7cc7b9ef8fa993c120e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 18:22:41 +0100 Subject: [PATCH 170/926] QA: Rename return variable inside meta() for clarity --- src/main/php/lang/ast/syntax/PHP.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index b75cc355..aa84d935 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1007,7 +1007,7 @@ private function annotations($parse, $context) { } private function meta($parse, $context) { - $annotations= [DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []]; + $meta= [DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []]; do { $parse->expecting('@', $context); @@ -1015,9 +1015,9 @@ private function meta($parse, $context) { $param= $parse->token->value; $parse->forward(); $parse->expecting(':', $context); - $a= &$annotations[DETAIL_TARGET_ANNO][$param]; + $a= &$meta[DETAIL_TARGET_ANNO][$param]; } else { - $a= &$annotations[DETAIL_ANNOTATIONS]; + $a= &$meta[DETAIL_ANNOTATIONS]; } $name= $parse->token->value; @@ -1042,7 +1042,7 @@ private function meta($parse, $context) { } while (null !== $parse->token->value); $parse->expecting(']', $context); - return $annotations; + return $meta; } private function parameters($parse) { From 6ff206119edac6a4de6bb68f08a5fc7554e4b41a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 17 Nov 2019 18:23:28 +0100 Subject: [PATCH 171/926] QA: Remove unused import --- src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index cf23610d..caa22e8e 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,6 +1,5 @@ Date: Sat, 23 Nov 2019 11:15:42 +0100 Subject: [PATCH 172/926] Do not misuse type members' annotations to hold target annotations Instead, seperate annotations and target annotations inside signature / parameter parsing --- src/main/php/lang/ast/emit/PHP.class.php | 10 +++--- src/main/php/lang/ast/syntax/PHP.class.php | 34 ++++++++++++------- .../ast/unittest/parse/MembersTest.class.php | 11 ++---- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 002f4fe2..ca7cbc94 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -377,7 +377,7 @@ protected function emitConst($result, $const) { protected function emitProperty($result, $property) { $result->meta[0][self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', - DETAIL_ANNOTATIONS => isset($property->annotations[DETAIL_ANNOTATIONS]) ? $property->annotations[DETAIL_ANNOTATIONS] : [], + DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, DETAIL_TARGET_ANNO => [], DETAIL_ARGUMENTS => [] @@ -396,9 +396,9 @@ protected function emitMethod($result, $method) { $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', - DETAIL_ANNOTATIONS => isset($method->annotations[DETAIL_ANNOTATIONS]) ? $method->annotations[DETAIL_ANNOTATIONS] : [], + DETAIL_ANNOTATIONS => $method->annotations, DETAIL_COMMENT => $method->comment, - DETAIL_TARGET_ANNO => isset($method->annotations[DETAIL_TARGET_ANNO]) ? $method->annotations[DETAIL_TARGET_ANNO] : [], + DETAIL_TARGET_ANNO => [], DETAIL_ARGUMENTS => [] ]; @@ -415,9 +415,7 @@ protected function emitMethod($result, $method) { DETAIL_ARGUMENTS => [] ]; } - foreach ($param->annotations as $name => $value) { - $meta[DETAIL_TARGET_ANNO][$param->name][$name]= $value; - } + $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } $result->out->write($declare); diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index aa84d935..ff2b39ca 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -834,11 +834,11 @@ public function __construct() { $parse->expecting(';', 'constant declaration'); }); - $this->body('@variable', function($parse, &$body, $annotations, $modifiers) { - $this->properties($parse, $body, $annotations, $modifiers, null); + $this->body('@variable', function($parse, &$body, $meta, $modifiers) { + $this->properties($parse, $body, $meta, $modifiers, null); }); - $this->body('function', function($parse, &$body, $annotations, $modifiers) { + $this->body('function', function($parse, &$body, $meta, $modifiers) { $line= $parse->token->line; $comment= $parse->comment; $parse->comment= null; @@ -851,7 +851,7 @@ public function __construct() { } $parse->forward(); - $signature= $this->signature($parse); + $signature= $this->signature($parse, isset($meta[DETAIL_TARGET_ANNO]) ? $meta[DETAIL_TARGET_ANNO] : []); if ('{' === $parse->token->value) { // Regular body $parse->forward(); @@ -864,7 +864,15 @@ public function __construct() { $parse->expecting('{ or ;', 'method declaration'); } - $body[$lookup]= new Method($modifiers, $name, $signature, $statements, $annotations, $comment, $line); + $body[$lookup]= new Method( + $modifiers, + $name, + $signature, + $statements, + isset($meta[DETAIL_ANNOTATIONS]) ? $meta[DETAIL_ANNOTATIONS] : [], + $comment, + $line + ); }); } @@ -943,9 +951,10 @@ private function type0($parse, $optional) { } } - private function properties($parse, &$body, $annotations, $modifiers, $type) { + private function properties($parse, &$body, $meta, $modifiers, $type) { $comment= $parse->comment; $parse->comment= null; + $annotations= isset($meta[DETAIL_ANNOTATIONS]) ? $meta[DETAIL_ANNOTATIONS] : []; while (';' !== $parse->token->value) { $line= $parse->token->line; @@ -966,10 +975,11 @@ private function properties($parse, &$body, $annotations, $modifiers, $type) { $parse->forward(); if ('=' === $parse->token->value) { $parse->forward(); - $body[$lookup]= new Property($modifiers, $name, $type, $this->expression($parse, 0), $annotations, $comment, $line); + $expr= $this->expression($parse, 0); } else { - $body[$lookup]= new Property($modifiers, $name, $type, null, $annotations, $comment, $line); + $expr= null; } + $body[$lookup]= new Property($modifiers, $name, $type, $expr, $annotations, $comment, $line); if (',' === $parse->token->value) { $parse->forward(); @@ -1045,7 +1055,7 @@ private function meta($parse, $context) { return $meta; } - private function parameters($parse) { + private function parameters($parse, $target) { static $promotion= ['private' => true, 'protected' => true, 'public' => true]; $parameters= []; @@ -1081,6 +1091,7 @@ private function parameters($parse) { } $name= $parse->token->value; + if (isset($target[$name])) $annotations= array_merge($annotations, $target[$name]); $parse->forward(); $default= null; @@ -1089,7 +1100,6 @@ private function parameters($parse) { $default= $this->expression($parse, 0); } $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); - $annotations= []; if (')' === $parse->token->value) { break; @@ -1154,9 +1164,9 @@ public function typeBody($parse) { return $body; } - public function signature($parse) { + public function signature($parse, $annotations= []) { $parse->expecting('(', 'signature'); - $parameters= $this->parameters($parse); + $parameters= $this->parameters($parse, $annotations); $parse->expecting(')', 'signature'); if (':' === $parse->token->value) { diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index e01e0548..1891166a 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -106,12 +106,8 @@ public function method_with_return_type() { #[@test] public function method_with_annotation() { - $annotations= [ - DETAIL_ANNOTATIONS => ['test' => null], - DETAIL_TARGET_ANNO => [] - ]; $body= [ - 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) + 'a()' => new Method(['public'], 'a', new Signature([], null), [], ['test' => null], null, self::LINE) ]; $this->assertParsed( [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], @@ -121,10 +117,7 @@ public function method_with_annotation() { #[@test] public function method_with_annotations() { - $annotations= [ - DETAIL_ANNOTATIONS => ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)], - DETAIL_TARGET_ANNO => [] - ]; + $annotations= ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)]; $body= [ 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) ]; From b58543ed0cc2d3129fcb206e33157108c2067b78 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 11:17:18 +0100 Subject: [PATCH 173/926] Lazily initialize target annotations --- src/main/php/lang/ast/syntax/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index ff2b39ca..85d51c88 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1144,7 +1144,7 @@ public function typeBody($parse) { $meta= []; } else if ('<<' === $parse->token->value) { $parse->forward(); - $meta= [DETAIL_ANNOTATIONS => $this->annotations($parse, 'member annotations'), DETAIL_TARGET_ANNO => []]; + $meta= [DETAIL_ANNOTATIONS => $this->annotations($parse, 'member annotations')]; } else if ('#[' === $parse->token->value) { $parse->forward(); $meta= $this->meta($parse, 'member annotations'); From f1733cce052bc66f7ddb6cf6acc8ffe3b82c010f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 11:18:10 +0100 Subject: [PATCH 174/926] Refactor code for consistency --- src/test/php/lang/ast/unittest/parse/MembersTest.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index 1891166a..138d257d 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -106,8 +106,9 @@ public function method_with_return_type() { #[@test] public function method_with_annotation() { + $annotations= ['test' => null]; $body= [ - 'a()' => new Method(['public'], 'a', new Signature([], null), [], ['test' => null], null, self::LINE) + 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) ]; $this->assertParsed( [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], From 811b8e69666a84857239945b8984efb603f96f7d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 11:48:11 +0100 Subject: [PATCH 175/926] QA: Add apidoc comments --- src/main/php/lang/ast/syntax/PHP.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 85d51c88..903dd588 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -988,6 +988,7 @@ private function properties($parse, &$body, $meta, $modifiers, $type) { $parse->expecting(';', 'field declaration'); } + /** Parses Hacklang-style annotations (<>) */ private function annotations($parse, $context) { $annotations= []; do { @@ -1016,6 +1017,7 @@ private function annotations($parse, $context) { return $annotations; } + /** Parses XP-style annotations (#[@test]) */ private function meta($parse, $context) { $meta= [DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []]; do { From c65f14f888e33c0ac6e5bddbb900a90187f8d7d9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 13:02:12 +0100 Subject: [PATCH 176/926] Add handling for key/value pairs See xp-framework/rfc#335 --- src/main/php/lang/ast/syntax/PHP.class.php | 33 ++++++++++++++++++- .../unittest/emit/AnnotationsTest.class.php | 9 +++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 903dd588..9160b04c 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1037,7 +1037,38 @@ private function meta($parse, $context) { if ('(' === $parse->token->value) { $parse->expecting('(', $context); - $a[$name]= $this->expression($parse, 0); + + if ('name' === $parse->token->kind) { + $token= $parse->token; + $parse->forward(); + $pairs= '=' === $parse->token->value; + + $parse->queue[]= $parse->token; + $parse->token= $token; + + if ($pairs) { + $line= $parse->token->line; + $values= []; + do { + $key= $parse->token->value; + $parse->forward(); + $parse->expecting('=', $context); + + $values[]= [new Literal("'".$key."'"), $this->expression($parse, 0)]; + + if (',' === $parse->token->value) { + $parse->forward(); + continue; + } + break; + } while (null !== $parse->token->value); + $a[$name]= new ArrayLiteral($values, $line); + } else { + $a[$name]= $this->expression($parse, 0); + } + } else { + $a[$name]= $this->expression($parse, 0); + } $parse->expecting(')', $context); } else { $a[$name]= null; diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index caa22e8e..100a06fb 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -106,6 +106,15 @@ class { }' $this->assertEquals(['test' => null], $t->getAnnotations()); } + #[@test] + public function xp_type_annotation_with_named_pairs() { + $t= $this->type(' + #[@resource(path= "/", authenticated= true)] + class { }' + ); + $this->assertEquals(['resource' => ['path' => '/', 'authenticated' => true]], $t->getAnnotations()); + } + #[@test] public function xp_type_annotations() { $t= $this->type(' From 167cbfd52e56f16252f4c45b3363d90e066bf17c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 13:11:30 +0100 Subject: [PATCH 177/926] Raise warnings for annotation key/value pair syntax --- src/main/php/lang/ast/syntax/PHP.class.php | 1 + src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 9160b04c..709d9239 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1051,6 +1051,7 @@ private function meta($parse, $context) { $values= []; do { $key= $parse->token->value; + $parse->warn('Use of deprecated annotation key/value pair "'.$key.'"', $context); $parse->forward(); $parse->expecting('=', $context); diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 100a06fb..53bc7870 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -106,6 +106,7 @@ class { }' $this->assertEquals(['test' => null], $t->getAnnotations()); } + /** @deprecated */ #[@test] public function xp_type_annotation_with_named_pairs() { $t= $this->type(' @@ -113,6 +114,7 @@ public function xp_type_annotation_with_named_pairs() { class { }' ); $this->assertEquals(['resource' => ['path' => '/', 'authenticated' => true]], $t->getAnnotations()); + \xp::gc(); } #[@test] From 24de100b12b07a2f1112b73b6b8831231fc2a375 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 13:17:17 +0100 Subject: [PATCH 178/926] Rewrite annotations to use map syntax instead of key/value pairs --- src/test/php/lang/ast/unittest/TokensTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/BracesTest.class.php | 4 ++-- src/test/php/lang/ast/unittest/emit/TernaryTest.class.php | 4 ++-- src/test/php/lang/ast/unittest/parse/TypesTest.class.php | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php index 6d94fd11..60c093e6 100755 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ b/src/test/php/lang/ast/unittest/TokensTest.class.php @@ -40,7 +40,7 @@ public function string_literals($input) { $this->assertTokens([['string' => $input]], new Tokens(new StringTokenizer($input))); } - #[@test, @expect(class= FormatException::class, withMessage= '/Unclosed string literal/'), @values([ + #[@test, @expect(['class' => FormatException::class, 'withMessage' => '/Unclosed string literal/']), @values([ # '"', # "'", # '"Test', diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index b885551a..b7713bc1 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -80,11 +80,11 @@ public function run() { $this->assertEquals(320, $r); } - #[@test, @values(map= [ + #[@test, @values(['map' => [ # '(__LINE__)."test"' => '3test', # '(__LINE__) + 1' => 4, # '(__LINE__) - 1' => 2, - #])] + #]])] public function global_constant_in_braces_not_confused_with_cast($input, $expected) { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index a735848a..5e917b59 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -2,7 +2,7 @@ class TernaryTest extends EmittingTest { - #[@test, @values(map= [true => "OK", false => "Fail"])] + #[@test, @values(['map' => [true => 'OK', false => 'Fail']])] public function ternary($value, $result) { $this->assertEquals($result, $this->run( 'class { @@ -14,7 +14,7 @@ public function run($value) { )); } - #[@test, @values(map= ["OK" => "OK", null => "Fail"])] + #[@test, @values(['map' => ['OK' => 'OK', null => 'Fail']])] public function short_ternary($value, $result) { $this->assertEquals($result, $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php index a02c1f22..e5e30391 100755 --- a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php @@ -123,17 +123,17 @@ public function class_in_namespace() { ); } - #[@test, @expect(class= Errors::class, withMessage= 'Cannot redeclare method b()')] + #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare method b()'])] public function cannot_redeclare_method() { iterator_to_array($this->parse('class A { public function b() { } public function b() { }}')); } - #[@test, @expect(class= Errors::class, withMessage= 'Cannot redeclare property $b')] + #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare property $b'])] public function cannot_redeclare_property() { iterator_to_array($this->parse('class A { public $b; private $b; }')); } - #[@test, @expect(class= Errors::class, withMessage= 'Cannot redeclare constant B')] + #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare constant B'])] public function cannot_redeclare_constant() { iterator_to_array($this->parse('class A { const B = 1; const B = 3; }')); } From e90743ee1150d59c27a73d012ca195b826c50822 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 16:15:10 +0100 Subject: [PATCH 179/926] Add support for #-style comments --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 2a3c4c52..57d1b50f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 4.3.0 / ????-??-?? + +* Merged PR #77: Add support for #-style comments including support for + XP style annotations + (@thekid) + ## 4.2.1 / 2019-10-05 * Fixed parser to allow "extending" final and abstract types - @thekid From b4df8049ab178c3717616df1ec9ee74d7f3f243c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 16:17:58 +0100 Subject: [PATCH 180/926] Rewrite annotations to use map syntax instead of key/value pairs --- .../unittest/loader/CompilingClassLoaderTest.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 3f2151a1..d53a5015 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -50,10 +50,10 @@ public function load_class() { $this->assertEquals('Tests', $this->load('Tests', 'getSimpleName()); } - #[@test, @expect( - # class= ClassFormatException::class, - # withMessage= 'Compiler error: Expected "{", have "(end)"' - #)] + #[@test, @expect([ + # 'class' => ClassFormatException::class, + # 'withMessage' => 'Compiler error: Expected "{", have "(end)"' + #])] public function load_class_with_syntax_errors() { $this->load('Errors', " Date: Sat, 23 Nov 2019 16:21:27 +0100 Subject: [PATCH 181/926] Raise warnings when encountering use of curly braces for offsets See https://wiki.php.net/rfc/deprecate_curly_braces_array_access --- src/main/php/lang/ast/syntax/PHP.class.php | 1 + .../ast/unittest/emit/LambdasTest.class.php | 17 +++++++++++------ .../ast/unittest/parse/VariablesTest.class.php | 2 ++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 709d9239..ccb10de8 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -177,6 +177,7 @@ public function __construct() { }); $this->infix('{', 80, function($parse, $token, $left) { + $parse->warn('Deprecated curly braces use as offset'); $expr= $this->expression($parse, 0); $parse->expecting('}', 'offset'); return new OffsetExpression($left, $expr, $token->line); diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index ea8b3d05..4c35d7bd 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -178,12 +178,17 @@ public function run() { $this->assertEquals(2, $r()); } - #[@test, @expect(Errors::class)] + #[@test] public function no_longer_supports_hacklang_variant() { - $this->run('class { - public function run() { - $func= ($arg) ==> { return 1; }; - } - }'); + try { + $this->run('class { + public function run() { + $func= ($arg) ==> { return 1; }; + } + }'); + $this->fail('No errors raised', null, Errors::class); + } catch (Errors $expected) { + \xp::gc(); + } } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php index 647a82d6..7be72164 100755 --- a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php @@ -39,11 +39,13 @@ public function array_offset() { ); } + /** @deprecated */ #[@test] public function string_offset() { $this->assertParsed( [new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE)], '$a{0};' ); + \xp::gc(); } } \ No newline at end of file From c8e33467554cda897924c46e719906e3adc7cf8a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Nov 2019 17:24:54 +0100 Subject: [PATCH 182/926] Fix an operator precedence problem for unary prefix operators --- ChangeLog.md | 3 + src/main/php/lang/ast/syntax/PHP.class.php | 28 ++++---- .../unittest/emit/PrecedenceTest.class.php | 14 ++++ .../ast/unittest/parse/OperatorTest.class.php | 69 +++++++++++++++++-- 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 57d1b50f..549241d2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 4.3.0 / ????-??-?? +* Fixed an operator precedence problem causing incorrect nesting in the + parsed AST for unary prefix operators. + (@thekid) * Merged PR #77: Add support for #-style comments including support for XP style annotations (@thekid) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 709d9239..61f20694 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -129,7 +129,7 @@ public function __construct() { } }); - $this->infix('->', 80, function($parse, $token, $left) { + $this->infix('->', 100, function($parse, $token, $left) { if ('{' === $parse->token->value) { $parse->forward(); $expr= $this->expression($parse, 0); @@ -142,7 +142,7 @@ public function __construct() { return new InstanceExpression($left, $expr, $token->line); }); - $this->infix('::', 80, function($parse, $token, $left) { + $this->infix('::', 100, function($parse, $token, $left) { $scope= $parse->scope->resolve($left->expression); if ('variable' === $parse->token->kind) { @@ -158,13 +158,13 @@ public function __construct() { return new ScopeExpression($scope, $expr, $token->line); }); - $this->infix('(', 80, function($parse, $token, $left) { + $this->infix('(', 100, function($parse, $token, $left) { $arguments= $this->expressions($parse); $parse->expecting(')', 'invoke expression'); return new InvokeExpression($left, $arguments, $token->line); }); - $this->infix('[', 80, function($parse, $token, $left) { + $this->infix('[', 100, function($parse, $token, $left) { if (']' === $parse->token->value) { $expr= null; $parse->forward(); @@ -176,7 +176,7 @@ public function __construct() { return new OffsetExpression($left, $expr, $token->line); }); - $this->infix('{', 80, function($parse, $token, $left) { + $this->infix('{', 100, function($parse, $token, $left) { $expr= $this->expression($parse, 0); $parse->expecting('}', 'offset'); return new OffsetExpression($left, $expr, $token->line); @@ -189,15 +189,15 @@ public function __construct() { return new TernaryExpression($left, $when, $else, $token->line); }); - $this->prefix('@', 100); - $this->prefix('&', 100); - $this->prefix('!', 100); - $this->prefix('~', 100); - $this->prefix('+', 100); - $this->prefix('-', 100); - $this->prefix('++', 100); - $this->prefix('--', 100); - $this->prefix('clone', 100); + $this->prefix('@', 90); + $this->prefix('&', 90); + $this->prefix('!', 90); + $this->prefix('~', 90); + $this->prefix('+', 90); + $this->prefix('-', 90); + $this->prefix('++', 90); + $this->prefix('--', 90); + $this->prefix('clone', 90); $this->assignment('='); $this->assignment('&='); diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index cf121551..ca782634 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -29,4 +29,18 @@ public function run() { ); $this->assertEquals('('.$t->getName().')', $t->newinstance()->run()); } + + #[@test] + public function plusplus() { + $t= $this->type( + 'class { + private $number= 1; + + public function run() { + return ++$this->number; + } + }' + ); + $this->assertEquals(2, $t->newinstance()->run()); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index 42ad571e..830053d9 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -7,6 +7,7 @@ use lang\ast\nodes\ClassDeclaration; use lang\ast\nodes\InstanceExpression; use lang\ast\nodes\InstanceOfExpression; +use lang\ast\nodes\InvokeExpression; use lang\ast\nodes\Literal; use lang\ast\nodes\NewClassExpression; use lang\ast\nodes\NewExpression; @@ -59,11 +60,11 @@ public function suffix($operator) { ); } - #[@test, @values(['!', '~', '-', '+', '++', '--'])] + #[@test, @values(['!', '~', '-', '+', '++', '--', '@', '&'])] public function prefix($operator) { $this->assertParsed( [new UnaryExpression('prefix', new Variable('a', self::LINE), $operator, self::LINE)], - ''.$operator.'$a;' + $operator.'$a;' ); } @@ -180,7 +181,15 @@ public function new_anonymous_implements() { } #[@test] - public function precedence_of_object_operator() { + public function scope_resolution_operator() { + $this->assertParsed( + [new ScopeExpression('\\Objects', new Literal('ID', self::LINE), self::LINE)], + 'Objects::ID;' + ); + } + + #[@test] + public function precedence_of_object_operator_binary() { $this->assertParsed( [new BinaryExpression( new InstanceExpression(new Variable('this', self::LINE), new Literal('a', self::LINE), self::LINE), @@ -193,7 +202,20 @@ public function precedence_of_object_operator() { } #[@test] - public function precedence_of_scope_resolution_operator() { + public function precedence_of_object_operator_unary() { + $this->assertParsed( + [new UnaryExpression( + 'prefix', + new InstanceExpression(new Variable('this', self::LINE), new Literal('a', self::LINE), self::LINE), + '!', + self::LINE + )], + '!$this->a;' + ); + } + + #[@test] + public function precedence_of_scope_resolution_operator_binary() { $this->assertParsed( [new BinaryExpression( new ScopeExpression('self', new Literal('class', self::LINE), self::LINE), @@ -205,6 +227,19 @@ public function precedence_of_scope_resolution_operator() { ); } + #[@test] + public function precedence_of_scope_resolution_operator_unary() { + $this->assertParsed( + [new UnaryExpression( + 'prefix', + new ScopeExpression('\\Objects', new Literal('ID', self::LINE), self::LINE), + '!', + self::LINE + )], + '!Objects::ID;' + ); + } + #[@test] public function precedence_of_not_and_instance_of() { $this->assertParsed( @@ -230,4 +265,30 @@ public function precedence_of_prefix($operator) { $operator.'2 === $value;' ); } + + #[@test] + public function precedence_of_braces_unary() { + $this->assertParsed( + [new UnaryExpression( + 'prefix', + new InvokeExpression(new Variable('a', self::LINE), [], self::LINE), + '!', + self::LINE + )], + '!$a();' + ); + } + + #[@test, @values(['!$a[0];', '!$a{0};'])] + public function precedence_of_offset_unary($input) { + $this->assertParsed( + [new UnaryExpression( + 'prefix', + new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE), + '!', + self::LINE + )], + $input + ); + } } \ No newline at end of file From 42d4130f2905ab81fabc17948443c3375921bc46 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Nov 2019 00:18:13 +0100 Subject: [PATCH 183/926] Fix emitting switch statements --- ChangeLog.md | 1 + src/main/php/lang/ast/emit/PHP.class.php | 2 +- .../emit/ControlStructuresTest.class.php | 46 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 549241d2..d7a8b777 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 4.3.0 / ????-??-?? +* Fixed emitting `switch` statements - @thekid * Fixed an operator precedence problem causing incorrect nesting in the parsed AST for unary prefix operators. (@thekid) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index ca7cbc94..552e1e9c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -528,7 +528,7 @@ protected function emitSwitch($result, $switch) { } else { $result->out->write('default:'); } - $this->emitOne($result, $case->body); + $this->emitAll($result, $case->body); } $result->out->write('}'); } diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php new file mode 100755 index 00000000..3c02a152 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -0,0 +1,46 @@ +run('class { + public function run($arg) { + if (0 === $arg) { + return "no items"; + } else if (1 === $arg) { + return "one item"; + } else { + return $arg." items"; + } + } + }', $input); + + $this->assertEquals($expected, $r); + } + + #[@test, @values([ + # [0, 'no items'], + # [1, 'one item'], + # [2, '2 items'], + # [3, '3 items'], + #])] + public function switch_case($input, $expected) { + $r= $this->run('class { + public function run($arg) { + switch ($arg) { + case 0: return "no items"; + case 1: return "one item"; + default: return $arg." items"; + } + } + }', $input); + + $this->assertEquals($expected, $r); + } +} \ No newline at end of file From f2113fc73dbaedef9ba541dc03068344d8fa5b96 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Nov 2019 00:19:34 +0100 Subject: [PATCH 184/926] Fix ambiguity between switch cases with constants and goto labels --- ChangeLog.md | 3 ++- src/main/php/lang/ast/syntax/PHP.class.php | 8 +++++++- .../emit/ControlStructuresTest.class.php | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index d7a8b777..8c7e2c07 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,8 @@ XP Compiler ChangeLog ## 4.3.0 / ????-??-?? -* Fixed emitting `switch` statements - @thekid +* Fixed emitting `switch` statements and case labels' ambiguity w/ goto + (@thekid) * Fixed an operator precedence problem causing incorrect nesting in the parsed AST for unary prefix operators. (@thekid) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 61f20694..ba3ae061 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -537,7 +537,13 @@ public function __construct() { $cases[]= new CaseLabel(null, [], $parse->token->line); } else if ('case' === $parse->token->value) { $parse->forward(); - $expr= $this->expression($parse, 0); + + if ('name' === $parse->token->kind) { + $expr= new Literal($parse->token->value, $parse->token->line); + $parse->forward(); + } else { + $expr= $this->expression($parse, 0); + } $parse->expecting(':', 'switch'); $cases[]= new CaseLabel($expr, [], $parse->token->line); } else { diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 3c02a152..12f02282 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -43,4 +43,20 @@ public function run($arg) { $this->assertEquals($expected, $r); } + + #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11]])] + public function switch_case_goto_label_ambiguity($whence, $expected) { + $r= $this->run('class { + public function run($arg) { + $position= 1; + switch ($arg) { + case SEEK_SET: $position= 10; break; + case SEEK_CUR: $position+= 10; break; + } + return $position; + } + }', $whence); + + $this->assertEquals($expected, $r); + } } \ No newline at end of file From 7082f2c65d4c0c432fb7b0f8b4b5051a9a212b0b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Nov 2019 00:51:59 +0100 Subject: [PATCH 185/926] Fix global constants in ternaries being ambiguous with goto labels --- ChangeLog.md | 2 ++ src/main/php/lang/ast/syntax/PHP.class.php | 27 ++++++++++++------- .../ast/unittest/emit/TernaryTest.class.php | 12 +++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8c7e2c07..082ac4e9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,8 @@ XP Compiler ChangeLog ## 4.3.0 / ????-??-?? +* Fixed global constants in ternaries being ambiguous with goto labels + (@thekid) * Fixed emitting `switch` statements and case labels' ambiguity w/ goto (@thekid) * Fixed an operator precedence problem causing incorrect nesting in the diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index ba3ae061..d93b78ff 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -412,15 +412,6 @@ public function __construct() { return new GotoStatement($label, $token->line); }); - $this->prefix('(name)', 0, function($parse, $token) { - if (':' === $parse->token->value) { - $parse->token= new Token($this->symbol(';')); - return new Label($token->value, $token->line); - } else { - return new Literal($token->value, $token->line); - } - }); - $this->prefix('(variable)', 0, function($parse, $token) { return new Variable($token->value, $token->line); }); @@ -429,6 +420,24 @@ public function __construct() { return new Literal($token->value, $token->line); }); + $this->prefix('(name)', 0, function($parse, $token) { + return new Literal($token->value, $token->line); + }); + + $this->stmt('(name)', function($parse, $token) { + + // Solve ambiguity between goto-labels and other statements + if (':' === $parse->token->value) { + $node= new Label($token->value, $token->line); + } else { + $parse->queue[]= $parse->token; + $parse->token= $token; + $node= $this->expression($parse, 0); + } + $parse->forward(); + return $node; + }); + $this->stmt('token->value; $parse->forward(); diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 5e917b59..ba33a829 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -14,6 +14,18 @@ public function run($value) { )); } + #[@test, @values(['map' => [true => MODIFIER_PUBLIC, false => MODIFIER_PRIVATE]])] + public function ternary_constants_goto_label_ambiguity($value, $result) { + $this->assertEquals($result, $this->run( + 'class { + public function run($value) { + return $value ? MODIFIER_PUBLIC : MODIFIER_PRIVATE; + } + }', + $value + )); + } + #[@test, @values(['map' => ['OK' => 'OK', null => 'Fail']])] public function short_ternary($value, $result) { $this->assertEquals($result, $this->run( From 560287bd30ba12dd664efae676567c238db7f370 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Nov 2019 16:54:07 +0100 Subject: [PATCH 186/926] Slightly speed up whitespace handling --- src/main/php/lang/ast/Tokens.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index 40596900..03f34d71 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -42,8 +42,7 @@ public function __construct(Tokenizer $source) { /** @return php.Iterator */ public function getIterator() { $line= 1; - while ($this->source->hasMoreTokens()) { - $token= $this->source->nextToken(); + while (null !== ($token= $this->source->nextToken())) { if ('$' === $token) { yield 'variable' => [$this->source->nextToken(), $line]; } else if ('"' === $token || "'" === $token) { @@ -62,9 +61,10 @@ public function getIterator() { yield 'string' => [$string, $line]; $line+= substr_count($string, "\n"); - } else if (0 === strcspn($token, " \r\n\t")) { - $line+= substr_count($token, "\n"); - continue; + } else if ("\n" === $token) { + $line++; + } else if ("\r" === $token || "\t" === $token || ' ' === $token) { + // Skip } else if (0 === strcspn($token, '0123456789')) { if ('.' === ($next= $this->source->nextToken())) { yield 'decimal' => [str_replace('_', '', $token.$next.$this->source->nextToken()), $line]; From e7bac3f818d113cab37da9686b6f365cfb4d7caf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Nov 2019 17:19:48 +0100 Subject: [PATCH 187/926] Prepare 4.3.0-RELEASE --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 082ac4e9..968478b2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 4.3.0 / ????-??-?? +## 4.3.0 / 2019-11-24 * Fixed global constants in ternaries being ambiguous with goto labels (@thekid) From 619841216d3700842378486bc552fe25f122191c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 29 Nov 2019 16:11:41 +0100 Subject: [PATCH 188/926] Remove deprecated curly braces --- src/test/php/lang/ast/unittest/parse/OperatorTest.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index d6714f14..21a46207 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -281,8 +281,8 @@ public function precedence_of_braces_unary() { ); } - #[@test, @values(['!$a[0];', '!$a{0};'])] - public function precedence_of_offset_unary($input) { + #[@test] + public function precedence_of_offset_unary() { $this->assertParsed( [new UnaryExpression( 'prefix', @@ -290,7 +290,7 @@ public function precedence_of_offset_unary($input) { '!', self::LINE )], - $input + '!$a[0];' ); } } \ No newline at end of file From 9fbb892ad432881d4873077307b6d2914a4fea5a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 29 Nov 2019 16:16:36 +0100 Subject: [PATCH 189/926] Deprecate curly braces --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d00b77a4..e7313a0c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,10 +5,14 @@ XP Compiler ChangeLog ## 5.0.0 / ????-??-?? +* Merged PR #78: Deprecate curly brace syntax for offsets; consistent + with PHP 7.4 + (@thekid) * Added support for XP 10 and newer versions of library dependencies (@thekid) * Implemented xp-framework/rfc#334: Drop PHP 5.6. The minimum required PHP version is now 7.0.0! + (@thekid) ## 4.3.0 / 2019-11-24 From d7347f05f254f58bbd126266960e2cde43f1bcb5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 09:39:59 +0100 Subject: [PATCH 190/926] MFB --- ChangeLog.md | 4 ++++ composer.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index e7313a0c..a35db621 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,6 +14,10 @@ XP Compiler ChangeLog PHP version is now 7.0.0! (@thekid) +## 4.3.1 / 2019-11-30 + +* Added compatibility with XP 10, see xp-framework/rfc#333 - @thekid + ## 4.3.0 / 2019-11-24 * Fixed global constants in ternaries being ambiguous with goto labels diff --git a/composer.json b/composer.json index dc2b2fcf..fa6fef04 100755 --- a/composer.json +++ b/composer.json @@ -7,8 +7,8 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "^4.0 | ^3.0", + "xp-framework/tokenize": "^8.1", + "xp-framework/ast": "^3.0", "php" : ">=7.0.0" }, "require-dev" : { From 0de3fb135b019fdb9cb53939e286fbb87d0497be Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 10:07:58 +0100 Subject: [PATCH 191/926] Convert testsuite to baseless tests --- composer.json | 2 +- .../lang/ast/unittest/EmitterTest.class.php | 11 ++--- .../ast/unittest/LineNumberTest.class.php | 5 ++- .../php/lang/ast/unittest/ScopeTest.class.php | 19 ++++---- .../lang/ast/unittest/TokensTest.class.php | 5 ++- .../unittest/emit/AnnotationsTest.class.php | 45 ++++++++++--------- .../emit/AnonymousClassTest.class.php | 9 ++-- .../emit/ArgumentPromotionTest.class.php | 9 ++-- .../emit/ArgumentUnpackingTest.class.php | 13 +++--- .../unittest/emit/ArrayTypesTest.class.php | 9 ++-- .../ast/unittest/emit/ArraysTest.class.php | 14 +++--- .../ast/unittest/emit/BlockTest.class.php | 6 ++- .../ast/unittest/emit/BracesTest.class.php | 18 ++++---- .../ast/unittest/emit/CastingTest.class.php | 28 +++++++----- .../ast/unittest/emit/CommentsTest.class.php | 18 ++++---- .../emit/CompactFunctionsTest.class.php | 7 +-- .../emit/ControlStructuresTest.class.php | 8 ++-- .../lang/ast/unittest/emit/EchoTest.class.php | 4 +- .../ast/unittest/emit/EmittingTest.class.php | 44 ++++++++---------- .../unittest/emit/ExceptionsTest.class.php | 11 ++--- .../unittest/emit/FunctionTypesTest.class.php | 5 ++- .../lang/ast/unittest/emit/GotoTest.class.php | 6 ++- .../ast/unittest/emit/ImportTest.class.php | 9 ++-- .../unittest/emit/InstanceOfTest.class.php | 12 ++--- .../unittest/emit/InvocationTest.class.php | 16 ++++--- .../ast/unittest/emit/LambdasTest.class.php | 29 ++++++------ .../ast/unittest/emit/LoopsTest.class.php | 18 ++++---- .../ast/unittest/emit/MembersTest.class.php | 18 ++++---- .../unittest/emit/MultipleCatchTest.class.php | 3 +- .../unittest/emit/NullCoalesceTest.class.php | 7 +-- .../ast/unittest/emit/NullSafeTest.class.php | 13 +++--- .../ast/unittest/emit/ParameterTest.class.php | 35 ++++++++------- .../unittest/emit/PrecedenceTest.class.php | 8 ++-- .../unittest/emit/PropertyTypesTest.class.php | 7 +-- .../ast/unittest/emit/ReturnTest.class.php | 8 ++-- .../ast/unittest/emit/ScalarsTest.class.php | 10 +++-- .../ast/unittest/emit/TernaryTest.class.php | 10 +++-- .../emit/TrailingCommasTest.class.php | 18 ++++---- .../ast/unittest/emit/TraitsTest.class.php | 11 ++--- .../emit/TransformationsTest.class.php | 18 ++++---- .../emit/TypeDeclarationTest.class.php | 25 ++++++----- .../emit/UnicodeEscapesTest.class.php | 6 ++- .../unittest/emit/UnionTypesTest.class.php | 7 +-- .../ast/unittest/emit/UsingTest.class.php | 15 ++++--- .../ast/unittest/emit/VarargsTest.class.php | 6 ++- .../ast/unittest/emit/YieldTest.class.php | 13 +++--- .../loader/CompilingClassLoaderTest.class.php | 9 ++-- .../ast/unittest/parse/BlocksTest.class.php | 1 + .../ast/unittest/parse/ClosuresTest.class.php | 4 +- .../ast/unittest/parse/CommentTest.class.php | 1 + .../parse/CompactFunctionsTest.class.php | 2 + .../unittest/parse/ConditionalTest.class.php | 2 + .../ast/unittest/parse/ErrorsTest.class.php | 3 +- .../unittest/parse/FunctionsTest.class.php | 3 +- .../ast/unittest/parse/InvokeTest.class.php | 1 + .../ast/unittest/parse/LambdasTest.class.php | 2 + .../ast/unittest/parse/LiteralsTest.class.php | 1 + .../ast/unittest/parse/LoopsTest.class.php | 2 + .../ast/unittest/parse/MembersTest.class.php | 1 + .../unittest/parse/NamespacesTest.class.php | 1 + .../ast/unittest/parse/OperatorTest.class.php | 1 + .../ast/unittest/parse/ParseTest.class.php | 9 ++-- .../unittest/parse/StartTokensTest.class.php | 1 + .../ast/unittest/parse/TypesTest.class.php | 1 + .../unittest/parse/VariablesTest.class.php | 1 + 65 files changed, 374 insertions(+), 290 deletions(-) diff --git a/composer.json b/composer.json index fa6fef04..3348c154 100755 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/unittest": "^10.0 | ^9.3" + "xp-framework/unittest": "^10.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index a3f10c37..598afa90 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -3,9 +3,10 @@ use io\streams\MemoryOutputStream; use lang\IllegalStateException; use lang\ast\{Emitter, Node, Result}; +use unittest\Assert; use unittest\TestCase; -class EmitterTest extends TestCase { +class EmitterTest { private function newEmitter() { return Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); @@ -18,7 +19,7 @@ public function can_create() { #[@test] public function transformations_initially_empty() { - $this->assertEquals([], $this->newEmitter()->transformations()); + Assert::equals([], $this->newEmitter()->transformations()); } #[@test] @@ -27,7 +28,7 @@ public function transform() { $fixture= $this->newEmitter(); $fixture->transform('class', $function); - $this->assertEquals(['class' => [$function]], $fixture->transformations()); + Assert::equals(['class' => [$function]], $fixture->transformations()); } #[@test] @@ -39,7 +40,7 @@ public function remove() { $transformation= $fixture->transform('class', $first); $fixture->transform('class', $second); $fixture->remove($transformation); - $this->assertEquals(['class' => [$second]], $fixture->transformations()); + Assert::equals(['class' => [$second]], $fixture->transformations()); } #[@test] @@ -49,7 +50,7 @@ public function remove_unsets_empty_kind() { $fixture= $this->newEmitter(); $transformation= $fixture->transform('class', $function); $fixture->remove($transformation); - $this->assertEquals([], $fixture->transformations()); + Assert::equals([], $fixture->transformations()); } #[@test, @expect(IllegalStateException::class)] diff --git a/src/test/php/lang/ast/unittest/LineNumberTest.class.php b/src/test/php/lang/ast/unittest/LineNumberTest.class.php index 1c49eb1b..8591fa90 100755 --- a/src/test/php/lang/ast/unittest/LineNumberTest.class.php +++ b/src/test/php/lang/ast/unittest/LineNumberTest.class.php @@ -2,8 +2,9 @@ use lang\ast\Tokens; use text\StringTokenizer; +use unittest\Assert; -class LineNumberTest extends \unittest\TestCase { +class LineNumberTest { /** * Assertion helper @@ -18,7 +19,7 @@ private function assertPositions($expected, $tokens) { foreach ($tokens as $type => $value) { $actual[]= [$value[0] => $value[1]]; } - $this->assertEquals($expected, $actual); + Assert::equals($expected, $actual); } #[@test] diff --git a/src/test/php/lang/ast/unittest/ScopeTest.class.php b/src/test/php/lang/ast/unittest/ScopeTest.class.php index e53540b4..1506414d 100755 --- a/src/test/php/lang/ast/unittest/ScopeTest.class.php +++ b/src/test/php/lang/ast/unittest/ScopeTest.class.php @@ -1,8 +1,9 @@ package('test'); - $this->assertEquals('\\test', $s->package); + Assert::equals('\\test', $s->package); } #[@test] public function resolve_in_global_scope() { $s= new Scope(); - $this->assertEquals('\\Parse', $s->resolve('Parse')); + Assert::equals('\\Parse', $s->resolve('Parse')); } #[@test] @@ -29,7 +30,7 @@ public function resolve_in_package() { $s= new Scope(); $s->package('test'); - $this->assertEquals('\\test\\Parse', $s->resolve('Parse')); + Assert::equals('\\test\\Parse', $s->resolve('Parse')); } #[@test] @@ -37,7 +38,7 @@ public function resolve_relative_in_package() { $s= new Scope(); $s->package('test'); - $this->assertEquals('\\test\\ast\\Parse', $s->resolve('ast\\Parse')); + Assert::equals('\\test\\ast\\Parse', $s->resolve('ast\\Parse')); } #[@test] @@ -46,7 +47,7 @@ public function resolve_imported_in_package() { $s->package('test'); $s->import('lang\\ast\\Parse'); - $this->assertEquals('\\lang\\ast\\Parse', $s->resolve('Parse')); + Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); } #[@test] @@ -54,7 +55,7 @@ public function resolve_imported_in_global_scope() { $s= new Scope(); $s->import('lang\\ast\\Parse'); - $this->assertEquals('\\lang\\ast\\Parse', $s->resolve('Parse')); + Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); } #[@test] @@ -62,7 +63,7 @@ public function package_inherited_from_parent() { $s= new Scope(); $s->package('test'); - $this->assertEquals('\\test\\Parse', (new Scope($s))->resolve('Parse')); + Assert::equals('\\test\\Parse', (new Scope($s))->resolve('Parse')); } #[@test] @@ -70,6 +71,6 @@ public function import_inherited_from_parent() { $s= new Scope(); $s->import('lang\\ast\\Parse'); - $this->assertEquals('\\lang\\ast\\Parse', (new Scope($s))->resolve('Parse')); + Assert::equals('\\lang\\ast\\Parse', (new Scope($s))->resolve('Parse')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php index 60c093e6..61b2f779 100755 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ b/src/test/php/lang/ast/unittest/TokensTest.class.php @@ -3,8 +3,9 @@ use lang\FormatException; use lang\ast\Tokens; use text\StringTokenizer; +use unittest\Assert; -class TokensTest extends \unittest\TestCase { +class TokensTest { /** * Assertion helper @@ -19,7 +20,7 @@ private function assertTokens($expected, $tokens) { foreach ($tokens as $type => $value) { $actual[]= [$type => $value[0]]; } - $this->assertEquals($expected, $actual); + Assert::equals($expected, $actual); } #[@test] diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 53bc7870..7287ac39 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,6 +1,7 @@ type('<> class { }'); - $this->assertEquals(['test' => null], $t->getAnnotations()); + Assert::equals(['test' => null], $t->getAnnotations()); } #[@test] public function primitive_value() { $t= $this->type('<> class { }'); - $this->assertEquals(['author' => 'Timm'], $t->getAnnotations()); + Assert::equals(['author' => 'Timm'], $t->getAnnotations()); } #[@test] public function array_value() { $t= $this->type('<> class { }'); - $this->assertEquals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); + Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); } #[@test] public function map_value() { $t= $this->type('< \lang\IllegalArgumentException::class])>> class { }'); - $this->assertEquals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); } #[@test] public function closure_value() { $t= $this->type('<> class { }'); $f= $t->getAnnotation('verify'); - $this->assertEquals('test', $f('test')); + Assert::equals('test', $f('test')); } #[@test] public function arrow_function_value() { $t= $this->type('< $arg)>> class { }'); $f= $t->getAnnotation('verify'); - $this->assertEquals('test', $f('test')); + Assert::equals('test', $f('test')); } #[@test] public function has_access_to_class() { $t= $this->type('<> class { const SUCCESS = true; }'); - $this->assertEquals(['expect' => true], $t->getAnnotations()); + Assert::equals(['expect' => true], $t->getAnnotations()); } #[@test] public function method() { $t= $this->type('class { <> public function fixture() { } }'); - $this->assertEquals(['test' => null], $t->getMethod('fixture')->getAnnotations()); + Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); } #[@test] public function field() { $t= $this->type('class { <> public $fixture; }'); - $this->assertEquals(['test' => null], $t->getField('fixture')->getAnnotations()); + Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); } #[@test] public function param() { $t= $this->type('class { public function fixture(<> $param) { } }'); - $this->assertEquals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); + Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); } #[@test] public function params() { $t= $this->type('class { public function fixture(< "a"])>> $a, <> $b) { } }'); $m=$t->getMethod('fixture'); - $this->assertEquals( + Assert::equals( [['inject' => ['name' => 'a']], ['inject' => null]], [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] ); @@ -88,13 +89,13 @@ public function params() { #[@test] public function multiple_class_annotations() { $t= $this->type('<> class { }'); - $this->assertEquals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } #[@test] public function multiple_member_annotations() { $t= $this->type('class { <> public function fixture() { } }'); - $this->assertEquals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); + Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } #[@test] @@ -103,7 +104,7 @@ public function xp_type_annotation() { #[@test] class { }' ); - $this->assertEquals(['test' => null], $t->getAnnotations()); + Assert::equals(['test' => null], $t->getAnnotations()); } /** @deprecated */ @@ -113,7 +114,7 @@ public function xp_type_annotation_with_named_pairs() { #[@resource(path= "/", authenticated= true)] class { }' ); - $this->assertEquals(['resource' => ['path' => '/', 'authenticated' => true]], $t->getAnnotations()); + Assert::equals(['resource' => ['path' => '/', 'authenticated' => true]], $t->getAnnotations()); \xp::gc(); } @@ -123,7 +124,7 @@ public function xp_type_annotations() { #[@resource("/"), @authenticated] class { }' ); - $this->assertEquals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } #[@test] @@ -135,7 +136,7 @@ public function xp_type_multiline() { class { }' ); $f= $t->getAnnotation('verify'); - $this->assertEquals('test', $f('test')); + Assert::equals('test', $f('test')); } #[@test] @@ -155,9 +156,9 @@ public function fails() { } public function cases() { } }' ); - $this->assertEquals(['test' => null], $t->getMethod('succeeds')->getAnnotations()); - $this->assertEquals(['test' => null, 'expect' => IllegalArgumentException::class], $t->getMethod('fails')->getAnnotations()); - $this->assertEquals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); + Assert::equals(['test' => null], $t->getMethod('succeeds')->getAnnotations()); + Assert::equals(['test' => null, 'expect' => IllegalArgumentException::class], $t->getMethod('fails')->getAnnotations()); + Assert::equals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); } #[@test] @@ -169,7 +170,7 @@ class { public function fixture($arg) { } }' ); - $this->assertEquals(['test' => null], $t->getMethod('fixture')->getAnnotations()); - $this->assertEquals(['inject' => 'conn'], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); + Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); + Assert::equals(['inject' => 'conn'], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 3b2fea3c..6bdcaa73 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -1,6 +1,7 @@ assertEquals('test', $r->id()); + Assert::equals('test', $r->id()); } #[@test] @@ -34,7 +35,7 @@ public function initialize() { }; } }'); - $this->assertInstanceOf(AbstractDeferredInvokationHandler::class, $r); + Assert::instance(AbstractDeferredInvokationHandler::class, $r); } #[@test] @@ -48,7 +49,7 @@ public function run() { }; } }'); - $this->assertInstanceOf(Runnable::class, $r); + Assert::instance(Runnable::class, $r); } #[@test] @@ -63,6 +64,6 @@ public function fixture() { } } }'); - $this->assertEquals(['inside' => null], typeof($r)->getMethod('fixture')->getAnnotations()); + Assert::equals(['inside' => null], typeof($r)->getMethod('fixture')->getAnnotations()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 140dd415..d0bd5400 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -1,6 +1,7 @@ id; } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -37,7 +38,7 @@ public function run() { return $this->id; } }'); - $this->assertEquals('tested', $r); + Assert::equals('tested', $r); } #[@test] @@ -51,7 +52,7 @@ public function run() { return $this->withId("test")->id; } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -60,7 +61,7 @@ public function type_information() { public function __construct(private int $id, private string $name) { } }'); - $this->assertEquals( + Assert::equals( [Primitive::$INT, Primitive::$STRING], [$t->getField('id')->getType(), $t->getField('name')->getType()] ); diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index 492de10b..2e36aced 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -1,6 +1,7 @@ fixture(...$args); } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } #[@test] @@ -31,7 +32,7 @@ public function run() { return [1, 2, ...$args]; } }'); - $this->assertEquals([1, 2, 3, 4], $r); + Assert::equals([1, 2, 3, 4], $r); } #[@test] @@ -41,7 +42,7 @@ public function run() { return [1, 2, ...[3, 4]]; } }'); - $this->assertEquals([1, 2, 3, 4], $r); + Assert::equals([1, 2, 3, 4], $r); } #[@test] @@ -52,7 +53,7 @@ public function run() { return ["color" => "red", ...$args, "year" => 2002]; } }'); - $this->assertEquals(['color' => 'red', 'type' => 'car', 'year' => 2002], $r); + Assert::equals(['color' => 'red', 'type' => 'car', 'year' => 2002], $r); } #[@test] @@ -67,7 +68,7 @@ public function run() { return [...$this->items()]; } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); } #[@test] @@ -81,6 +82,6 @@ public function run() { return [...$this->items()]; } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php index 30f286db..38f206cd 100755 --- a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php @@ -1,5 +1,6 @@ $test; }'); - $this->assertEquals('int[]', $t->getField('test')->getType()->getName()); + Assert::equals('int[]', $t->getField('test')->getType()->getName()); } #[@test] @@ -22,7 +23,7 @@ public function int_map_type() { private array $test; }'); - $this->assertEquals('[:int]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int]', $t->getField('test')->getType()->getName()); } #[@test] @@ -31,7 +32,7 @@ public function nested_map_type() { private array> $test; }'); - $this->assertEquals('[:int[]]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int[]]', $t->getField('test')->getType()->getName()); } #[@test] @@ -40,6 +41,6 @@ public function var_map_type() { private array $test; }'); - $this->assertEquals('[:var]', $t->getField('test')->getType()->getName()); + Assert::equals('[:var]', $t->getField('test')->getType()->getName()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 1ce202f4..dee00e21 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,5 +1,7 @@ assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } #[@test] @@ -21,7 +23,7 @@ public function run() { } }'); - $this->assertEquals(['a' => 1, 'b' => 2], $r); + Assert::equals(['a' => 1, 'b' => 2], $r); } #[@test] @@ -34,7 +36,7 @@ public function run() { } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } #[@test] @@ -46,7 +48,7 @@ public function run() { } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); } #[@test] @@ -58,7 +60,7 @@ public function run() { } }'); - $this->assertEquals(['key' => 'value'], $r); + Assert::equals(['key' => 'value'], $r); } #[@test] @@ -70,6 +72,6 @@ public function run() { } }'); - $this->assertEquals(['key' => 'value'], $r); + Assert::equals(['key' => 'value'], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php index 5d12740c..1ff3bdeb 100755 --- a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php @@ -1,5 +1,7 @@ assertTrue($r); + Assert::true($r); } #[@test] @@ -26,6 +28,6 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index b7713bc1..f9371ccd 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -1,5 +1,7 @@ assertEquals('test1', $r); + Assert::equals('test1', $r); } #[@test] @@ -23,7 +25,7 @@ public function run() { } }'); - $this->assertEquals(250905600, $r); + Assert::equals(250905600, $r); } #[@test] @@ -34,7 +36,7 @@ public function run() { } }'); - $this->assertEquals(250905600, $r); + Assert::equals(250905600, $r); } #[@test] @@ -51,7 +53,7 @@ public function run() { } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -64,7 +66,7 @@ public function run() { } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -77,7 +79,7 @@ public function run() { } }'); - $this->assertEquals(320, $r); + Assert::equals(320, $r); } #[@test, @values(['map' => [ @@ -92,7 +94,7 @@ public function run() { } }'); - $this->assertEquals($expected, $r); + Assert::equals($expected, $r); } #[@test] @@ -106,6 +108,6 @@ public function run() { } }'); - $this->assertEquals('OK', $r); + Assert::equals('OK', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php index 6fe8dc6f..8e03bd7c 100755 --- a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php @@ -1,16 +1,20 @@ assertEquals((string)$value, $this->run( + Assert::equals((string)$value, $this->run( 'class { public function run($value) { return (string)$value; @@ -28,7 +32,7 @@ public function run($value) { # true, false #])] public function int_cast($value) { - $this->assertEquals((int)$value, $this->run( + Assert::equals((int)$value, $this->run( 'class { public function run($value) { return (int)$value; @@ -45,7 +49,7 @@ public function run($value) { # null, false, true, 1, 1.5, "", "test" #])] public function array_cast($value) { - $this->assertEquals((array)$value, $this->run( + Assert::equals((array)$value, $this->run( 'class { public function run($value) { return (array)$value; @@ -57,7 +61,7 @@ public function run($value) { #[@test] public function value_cast() { - $this->assertEquals($this, $this->run( + Assert::equals($this, $this->run( 'class { public function run($value) { return (\lang\ast\unittest\emit\CastingTest)$value; @@ -69,7 +73,7 @@ public function run($value) { #[@test] public function int_array_cast() { - $this->assertEquals([1, 2, 3], $this->run( + Assert::equals([1, 2, 3], $this->run( 'class { public function run($value) { return (array)$value; @@ -90,7 +94,7 @@ public function run() { #[@test, @values([null, 'test'])] public function nullable_string_cast_of($value) { - $this->assertEquals($value, $this->run( + Assert::equals($value, $this->run( 'class { public function run($value) { return (?string)$value; @@ -102,7 +106,7 @@ public function run($value) { #[@test] public function cast_braced() { - $this->assertEquals(['test'], $this->run( + Assert::equals(['test'], $this->run( 'class { public function run($value) { return (array)($value); @@ -114,19 +118,19 @@ public function run($value) { #[@test] public function cast_to_function_type_and_invoke() { - $this->assertEquals($this->getName(), $this->run( + Assert::equals($this->test(), $this->run( 'class { public function run($value) { return ((function(): string)($value))(); } }', - [$this, 'getName'] + [$this, 'test'] )); } #[@test] public function object_cast_on_literal() { - $this->assertEquals((object)['key' => 'value'], $this->run( + Assert::equals((object)['key' => 'value'], $this->run( 'class { public function run($value) { return (object)["key" => "value"]; @@ -142,7 +146,7 @@ public function run($value) { # [null, null] #])] public function nullable_int($value, $expected) { - $this->assertEquals($expected, $this->run( + Assert::equals($expected, $this->run( 'class { public function run($value) { return (?int)$value; @@ -154,7 +158,7 @@ public function run($value) { #[@test, @values([new Handle(10), null])] public function nullable_value($value) { - $this->assertEquals($value, $this->run( + Assert::equals($value, $this->run( 'class { public function run($value) { return (?\lang\ast\unittest\emit\Handle)$value; diff --git a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php index 13251764..31c233b3 100755 --- a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php @@ -1,23 +1,25 @@ type('/** Test */ class { }'); - $this->assertEquals('Test', $t->getComment()); + Assert::equals('Test', $t->getComment()); } #[@test] public function on_interface() { $t= $this->type('/** Test */ interface { }'); - $this->assertEquals('Test', $t->getComment()); + Assert::equals('Test', $t->getComment()); } #[@test] public function on_trait() { $t= $this->type('/** Test */ trait { }'); - $this->assertEquals('Test', $t->getComment()); + Assert::equals('Test', $t->getComment()); } #[@test] @@ -30,13 +32,13 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->getMethod('fixture')->getComment()); } #[@test] public function comments_are_escaped() { $t= $this->type("/** Timm's test */ class { }"); - $this->assertEquals("Timm's test", $t->getComment()); + Assert::equals("Timm's test", $t->getComment()); } #[@test] @@ -51,7 +53,7 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->getMethod('fixture')->getComment()); } #[@test] @@ -66,7 +68,7 @@ public function fixture() { /** Not the right comment */ }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->getMethod('fixture')->getComment()); } #[@test] @@ -79,6 +81,6 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->getMethod('fixture')->getComment()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php index 7b2df15b..6da92a02 100755 --- a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php @@ -1,5 +1,6 @@ run('class { public fn run() => "test"; }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -23,7 +24,7 @@ public function with_property() { public fn run() => $this->id; }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } #[@test] @@ -36,6 +37,6 @@ public function run() { return $this->withId("test")->id(); } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 12f02282..84d78680 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -1,5 +1,7 @@ assertEquals($expected, $r); + Assert::equals($expected, $r); } #[@test, @values([ @@ -41,7 +43,7 @@ public function run($arg) { } }', $input); - $this->assertEquals($expected, $r); + Assert::equals($expected, $r); } #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11]])] @@ -57,6 +59,6 @@ public function run($arg) { } }', $whence); - $this->assertEquals($expected, $r); + Assert::equals($expected, $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index b9e25fdf..6e951cca 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -1,5 +1,7 @@ "Hello"; public function run() { '.$statement.' } }'); - $this->assertEquals($expected, ob_get_contents()); + Assert::equals($expected, ob_get_contents()); } finally { ob_end_clean(); } diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index e7f21505..c05a4c91 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -4,40 +4,34 @@ use lang\DynamicClassLoader; use lang\ast\{CompilingClassLoader, Emitter, Language, Node, Parse, Result, Tokens}; use text\StringTokenizer; +use unittest\Assert; use unittest\TestCase; use util\cmd\Console; -abstract class EmittingTest extends TestCase { - private static $cl, $language, $emitter; +abstract class EmittingTest { private static $id= 0; - private $output; + private $cl, $language, $emitter, $output; private $transformations= []; - #[@beforeClass] - public static function setupCompiler() { - self::$cl= DynamicClassLoader::instanceFor(self::class); - self::$language= Language::named('PHP'); - self::$emitter= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); - foreach (self::$language->extensions() as $extension) { - $extension->setup(self::$language, self::$emitter); - } - } - /** * Constructor * - * @param string $name * @param ?string $output E.g. `ast,code` to dump both AST and emitted code */ - public function __construct($name, $output= null) { - parent::__construct($name); + public function __construct($output= null) { $this->output= $output ? array_flip(explode(',', $output)) : []; + $this->cl= DynamicClassLoader::instanceFor(self::class); + $this->language= Language::named('PHP'); + $this->emitter= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); + foreach ($this->language->extensions() as $extension) { + $extension->setup($this->language, $this->emitter); + } } - /** @return void */ + #[@after] public function tearDown() { foreach ($this->transformations as $transformation) { - self::$emitter->remove($transformation); + $this->emitter->remove($transformation); } } @@ -49,7 +43,7 @@ public function tearDown() { * @return void */ protected function transform($type, $function) { - $this->transformations[]= self::$emitter->transform($type, $function); + $this->transformations[]= $this->emitter->transform($type, $function); } /** @@ -62,23 +56,23 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= new Parse(self::$language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); + $parse= new Parse($this->language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), static::class); $ast= iterator_to_array($parse->execute()); if (isset($this->output['ast'])) { Console::writeLine(); - Console::writeLine('=== ', $this->name, ' ==='); + Console::writeLine('=== ', static::class, ' ==='); Console::writeLine($ast); } - self::$emitter->emitAll(new Result(new StringWriter($out)), $ast); + $this->emitter->emitAll(new Result(new StringWriter($out)), $ast); if (isset($this->output['code'])) { Console::writeLine(); - Console::writeLine('=== ', $this->name, ' ==='); + Console::writeLine('=== ', static::class, ' ==='); Console::writeLine($out->getBytes()); } - self::$cl->setClassBytes($name, $out->getBytes()); - return self::$cl->loadClass($name); + $this->cl->setClassBytes($name, $out->getBytes()); + return $this->cl->loadClass($name); } /** diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 4ee28f1b..2a83e6ac 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -1,6 +1,7 @@ assertEquals(IllegalArgumentException::class, $t->newInstance()->run()); + Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } #[@test] @@ -31,7 +32,7 @@ public function run() { } }'); - $this->assertEquals(4, $t->newInstance()->run()); + Assert::equals(4, $t->newInstance()->run()); } #[@test] @@ -46,7 +47,7 @@ public function run() { } }'); - $this->assertEquals(IllegalArgumentException::class, $t->newInstance()->run()); + Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } #[@test] @@ -64,7 +65,7 @@ public function run() { $instance= $t->newInstance(); $instance->run(); - $this->assertTrue($instance->closed); + Assert::true($instance->closed); } #[@test] @@ -85,7 +86,7 @@ public function run() { $instance->run(); $this->fail('Expected exception not caught', null, IllegalArgumentException::class); } catch (IllegalArgumentException $expected) { - $this->assertTrue($instance->closed); + Assert::true($instance->closed); } } diff --git a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php index dd8d53e0..534b64cc 100755 --- a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php @@ -1,6 +1,7 @@ assertEquals( + Assert::equals( new FunctionType([], Primitive::$STRING), $t->getField('test')->getType() ); @@ -28,7 +29,7 @@ public function function_with_parameters() { private (function(int, string): string) $test; }'); - $this->assertEquals( + Assert::equals( new FunctionType([Primitive::$INT, Primitive::$STRING], Primitive::$STRING), $t->getField('test')->getType() ); diff --git a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php index ff57865e..74b259bc 100755 --- a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php @@ -1,5 +1,7 @@ assertTrue($r); + Assert::true($r); } #[@test] @@ -28,6 +30,6 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index f9a6eb98..a1552940 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -1,6 +1,7 @@ assertEquals(Date::class, $this->run(' + Assert::equals(Date::class, $this->run(' use util\Date; class { @@ -22,7 +23,7 @@ public function run() { return Date::class; } #[@test] public function import_type_as_alias() { - $this->assertEquals(Date::class, $this->run(' + Assert::equals(Date::class, $this->run(' use util\Date as D; class { @@ -33,7 +34,7 @@ public function run() { return D::class; } #[@test] public function import_const() { - $this->assertEquals('imported', $this->run(' + Assert::equals('imported', $this->run(' use const lang\ast\unittest\emit\FIXTURE; class { @@ -44,7 +45,7 @@ public function run() { return FIXTURE; } #[@test] public function import_function() { - $this->assertEquals('imported', $this->run(' + Assert::equals('imported', $this->run(' use function lang\ast\unittest\emit\fixture; class { diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index f126eca6..004ddbda 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -1,5 +1,7 @@ assertTrue($r); + Assert::true($r); } #[@test] @@ -21,7 +23,7 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -32,7 +34,7 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -43,7 +45,7 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -54,6 +56,6 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index 99b989d5..5b8c5f9f 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -1,10 +1,12 @@ assertEquals('instance', $this->run( + Assert::equals('instance', $this->run( 'class { public function instanceMethod() { return "instance"; } @@ -18,7 +20,7 @@ public function run() { #[@test] public function instance_method_dynamic_variable() { - $this->assertEquals('instance', $this->run( + Assert::equals('instance', $this->run( 'class { public function instanceMethod() { return "instance"; } @@ -33,7 +35,7 @@ public function run() { #[@test] public function instance_method_dynamic_expression() { - $this->assertEquals('instance', $this->run( + Assert::equals('instance', $this->run( 'class { public function instanceMethod() { return "instance"; } @@ -48,7 +50,7 @@ public function run() { #[@test] public function static_method() { - $this->assertEquals('static', $this->run( + Assert::equals('static', $this->run( 'class { public function staticMethod() { return "static"; } @@ -62,7 +64,7 @@ public function run() { #[@test] public function static_method_dynamic() { - $this->assertEquals('static', $this->run( + Assert::equals('static', $this->run( 'class { public static function staticMethod() { return "static"; } @@ -77,7 +79,7 @@ public function run() { #[@test] public function closure() { - $this->assertEquals('closure', $this->run( + Assert::equals('closure', $this->run( 'class { public function run() { @@ -90,7 +92,7 @@ public function run() { #[@test] public function global_function() { - $this->assertEquals('function', $this->run( + Assert::equals('function', $this->run( 'function fixture() { return "function"; } class { diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 4c35d7bd..232ac285 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -1,6 +1,7 @@ assertEquals(2, $r(1)); + Assert::equals(2, $r(1)); } #[@test] @@ -29,7 +30,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1, 2)); + Assert::equals(3, $r(1, 2)); } #[@test] @@ -42,7 +43,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -54,7 +55,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -69,7 +70,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -82,7 +83,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -94,7 +95,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -105,7 +106,7 @@ public function run($addend) { } }', 2); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -117,7 +118,7 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } #[@test] @@ -128,7 +129,7 @@ public function run() { } }'); - $this->assertEquals('lang.Value', typeof($r)->signature()[0]->getName()); + Assert::equals('lang.Value', typeof($r)->signature()[0]->getName()); } #[@test, @action(new RuntimeVersion('>=7.0'))] @@ -139,7 +140,7 @@ public function run() { } }'); - $this->assertEquals('lang.Value', typeof($r)->returns()->getName()); + Assert::equals('lang.Value', typeof($r)->returns()->getName()); } #[@test] @@ -150,7 +151,7 @@ public function run() { } }'); - $this->assertEquals(1, $r()); + Assert::equals(1, $r()); } #[@test] @@ -161,7 +162,7 @@ public function run() { } }'); - $this->assertEquals('IIFE', $r); + Assert::equals('IIFE', $r); } #[@test] @@ -175,7 +176,7 @@ public function run() { } }'); - $this->assertEquals(2, $r()); + Assert::equals(2, $r()); } #[@test] diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index 31f039a7..c351211a 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -1,5 +1,7 @@ assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); } #[@test] @@ -29,7 +31,7 @@ public function run() { } }'); - $this->assertEquals('a=1,b=2,c=3', $r); + Assert::equals('a=1,b=2,c=3', $r); } #[@test] @@ -42,7 +44,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); } #[@test] @@ -57,7 +59,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); } #[@test] @@ -73,7 +75,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3,4', $r); + Assert::equals('1,2,3,4', $r); } #[@test] @@ -89,7 +91,7 @@ public function run() { } }'); - $this->assertEquals('1,2,3,4', $r); + Assert::equals('1,2,3,4', $r); } #[@test] @@ -109,7 +111,7 @@ public function run() { } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } #[@test] @@ -129,6 +131,6 @@ public function run() { } }'); - $this->assertEquals([2, 3, 4], $r); + Assert::equals([2, 3, 4], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index f0827447..74612ee9 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,5 +1,7 @@ assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -25,7 +27,7 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -38,7 +40,7 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -51,7 +53,7 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -68,7 +70,7 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -81,7 +83,7 @@ public function run() { } }'); - $this->assertEquals('MON', $r); + Assert::equals('MON', $r); } #[@test] @@ -93,7 +95,7 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -105,6 +107,6 @@ public function run() { } }'); - $this->assertNull($r); + Assert::null($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php index e46017ab..cde6d4fa 100755 --- a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php @@ -1,6 +1,7 @@ assertEquals('caught '.$type, $t->newInstance()->run($type)); + Assert::equals('caught '.$type, $t->newInstance()->run($type)); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php index 51280dcd..a714edfd 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php @@ -1,5 +1,6 @@ assertTrue($r); + Assert::true($r); } #[@test] @@ -23,7 +24,7 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -35,6 +36,6 @@ public function run() { } }'); - $this->assertEquals(['key' => true], $r); + Assert::equals(['key' => true], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index cf65dc9f..214b38ba 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -1,6 +1,7 @@ assertNull($r); + Assert::null($r); } #[@test] @@ -34,7 +35,7 @@ public function method() { return true; } } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -46,7 +47,7 @@ public function run() { } }'); - $this->assertNull($r); + Assert::null($r); } #[@test] @@ -60,7 +61,7 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } #[@test] @@ -81,7 +82,7 @@ public function run() { } '); - $this->assertEquals([null, ['outer', 'inner']], $r); + Assert::equals([null, ['outer', 'inner']], $r); } #[@test] @@ -98,6 +99,6 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index e568e4a1..94612827 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -1,6 +1,7 @@ assertEquals('param', $this->param('$param')->getName()); + Assert::equals('param', $this->param('$param')->getName()); } #[@test] public function without_type() { - $this->assertEquals(Type::$VAR, $this->param('$param')->getType()); + Assert::equals(Type::$VAR, $this->param('$param')->getType()); } #[@test, @values([ @@ -34,62 +35,62 @@ public function without_type() { # ['object $param', Type::$OBJECT], #])] public function with_special_type($declaration, $type) { - $this->assertEquals($type, $this->param($declaration)->getType()); + Assert::equals($type, $this->param($declaration)->getType()); } #[@test] public function value_typed() { - $this->assertEquals(new XPClass(Value::class), $this->param('Value $param')->getType()); + Assert::equals(new XPClass(Value::class), $this->param('Value $param')->getType()); } #[@test] public function value_type_with_null() { - $this->assertEquals(new XPClass(Value::class), $this->param('Value $param= null')->getType()); + Assert::equals(new XPClass(Value::class), $this->param('Value $param= null')->getType()); } #[@test] public function nullable_value_type() { - $this->assertEquals(new XPClass(Value::class), $this->param('?Value $param')->getType()); + Assert::equals(new XPClass(Value::class), $this->param('?Value $param')->getType()); } #[@test] public function string_typed() { - $this->assertEquals(Primitive::$STRING, $this->param('string $param')->getType()); + Assert::equals(Primitive::$STRING, $this->param('string $param')->getType()); } #[@test] public function string_typed_with_null() { - $this->assertEquals(Primitive::$STRING, $this->param('string $param= null')->getType()); + Assert::equals(Primitive::$STRING, $this->param('string $param= null')->getType()); } #[@test] public function nullable_string_type() { - $this->assertEquals(Primitive::$STRING, $this->param('?string $param')->getType()); + Assert::equals(Primitive::$STRING, $this->param('?string $param')->getType()); } #[@test] public function array_typed() { - $this->assertEquals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); + Assert::equals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); } #[@test] public function map_typed() { - $this->assertEquals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); + Assert::equals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); } #[@test] public function simple_annotation() { - $this->assertEquals(['inject' => null], $this->param('<> $param')->getAnnotations()); + Assert::equals(['inject' => null], $this->param('<> $param')->getAnnotations()); } #[@test] public function annotation_with_value() { - $this->assertEquals(['inject' => 'dsn'], $this->param('<> $param')->getAnnotations()); + Assert::equals(['inject' => 'dsn'], $this->param('<> $param')->getAnnotations()); } #[@test] public function multiple_annotations() { - $this->assertEquals( + Assert::equals( ['inject' => null, 'name' => 'dsn'], $this->param('<> $param')->getAnnotations() ); @@ -97,16 +98,16 @@ public function multiple_annotations() { #[@test] public function required_parameter() { - $this->assertEquals(false, $this->param('$param')->isOptional()); + Assert::equals(false, $this->param('$param')->isOptional()); } #[@test] public function optional_parameter() { - $this->assertEquals(true, $this->param('$param= true')->isOptional()); + Assert::equals(true, $this->param('$param= true')->isOptional()); } #[@test] public function optional_parameters_default_value() { - $this->assertEquals(true, $this->param('$param= true')->getDefaultValue()); + Assert::equals(true, $this->param('$param= true')->getDefaultValue()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index fe5d182b..4995f0af 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -1,5 +1,7 @@ assertEquals($result, $this->run( + Assert::equals($result, $this->run( 'class { public function run() { return '.$input.'; @@ -27,7 +29,7 @@ public function run() { } }' ); - $this->assertEquals('('.$t->getName().')', $t->newInstance()->run()); + Assert::equals('('.$t->getName().')', $t->newInstance()->run()); } #[@test] @@ -41,6 +43,6 @@ public function run() { } }' ); - $this->assertEquals(2, $t->newinstance()->run()); + Assert::equals(2, $t->newinstance()->run()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index eb40b901..9c5d99c4 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -1,5 +1,6 @@ assertEquals('int', $t->getField('test')->getTypeName()); + Assert::equals('int', $t->getField('test')->getTypeName()); } #[@test] @@ -23,7 +24,7 @@ public function self_type() { private static self $instance; }'); - $this->assertEquals('self', $t->getField('instance')->getTypeName()); + Assert::equals('self', $t->getField('instance')->getTypeName()); } #[@test] @@ -32,6 +33,6 @@ public function interface_type() { private \\lang\\Value $value; }'); - $this->assertEquals('lang.Value', $t->getField('value')->getTypeName()); + Assert::equals('lang.Value', $t->getField('value')->getTypeName()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php index b1132cd7..351473ba 100755 --- a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php @@ -1,5 +1,7 @@ assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -21,7 +23,7 @@ public function run() { return $this->member; } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] @@ -31,6 +33,6 @@ public function run() { return; } }'); - $this->assertEquals(null, $r); + Assert::equals(null, $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index f5e8d30c..67f03207 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -1,5 +1,7 @@ assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } #[@test, @values([ @@ -23,7 +25,7 @@ public function numbers($literal, $result) { # ['0137_041', 48673], #])] public function numeric_literal_separator($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } #[@test, @values([ @@ -31,7 +33,7 @@ public function numeric_literal_separator($literal, $result) { # ['"Test"', 'Test'], #])] public function strings($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } #[@test, @values([ @@ -40,6 +42,6 @@ public function strings($literal, $result) { # ['null', null], #])] public function constants($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index ba33a829..3c900034 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -1,10 +1,12 @@ [true => 'OK', false => 'Fail']])] public function ternary($value, $result) { - $this->assertEquals($result, $this->run( + Assert::equals($result, $this->run( 'class { public function run($value) { return $value ? "OK" : "Fail"; @@ -16,7 +18,7 @@ public function run($value) { #[@test, @values(['map' => [true => MODIFIER_PUBLIC, false => MODIFIER_PRIVATE]])] public function ternary_constants_goto_label_ambiguity($value, $result) { - $this->assertEquals($result, $this->run( + Assert::equals($result, $this->run( 'class { public function run($value) { return $value ? MODIFIER_PUBLIC : MODIFIER_PRIVATE; @@ -28,7 +30,7 @@ public function run($value) { #[@test, @values(['map' => ['OK' => 'OK', null => 'Fail']])] public function short_ternary($value, $result) { - $this->assertEquals($result, $this->run( + Assert::equals($result, $this->run( 'class { public function run($value) { return $value ?: "Fail"; @@ -40,7 +42,7 @@ public function run($value) { #[@test, @values([[['OK']], [[]]])] public function null_coalesce($value) { - $this->assertEquals('OK', $this->run( + Assert::equals('OK', $this->run( 'class { public function run($value) { return $value[0] ?? "OK"; diff --git a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php index 0bd202be..94fd92d0 100755 --- a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php @@ -1,52 +1,54 @@ run('class { public function run() { return ["test", ]; } }'); - $this->assertEquals(['test'], $r); + Assert::equals(['test'], $r); } #[@test] public function in_map() { $r= $this->run('class { public function run() { return ["test" => true, ]; } }'); - $this->assertEquals(['test' => true], $r); + Assert::equals(['test' => true], $r); } #[@test] public function in_function_call() { $r= $this->run('class { public function run() { return sprintf("Hello %s", "test", ); } }'); - $this->assertEquals('Hello test', $r); + Assert::equals('Hello test', $r); } #[@test] public function in_parameter_list() { $r= $this->run('class { public function run($a, ) { return $a; } }', 'Test'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } #[@test] public function in_isset() { $r= $this->run('class { public function run() { return isset($a, ); } }'); - $this->assertEquals(false, $r); + Assert::equals(false, $r); } #[@test] public function in_list() { $r= $this->run('class { public function run() { list($a, )= [1, 2]; return $a; } }'); - $this->assertEquals(1, $r); + Assert::equals(1, $r); } #[@test] public function in_short_list() { $r= $this->run('class { public function run() { [$a, ]= [1, 2]; return $a; } }'); - $this->assertEquals(1, $r); + Assert::equals(1, $r); } #[@test] public function in_namespace_group() { $r= $this->run('use lang\\{Type, }; class { public function run() { return Type::$ARRAY->getName(); } }'); - $this->assertEquals('array', $r); + Assert::equals('array', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php index 084de57c..7618227a 100755 --- a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php @@ -1,6 +1,7 @@ type('class { use \lang\ast\unittest\emit\Loading; }'); - $this->assertEquals([new XPClass(Loading::class)], $t->getTraits()); + Assert::equals([new XPClass(Loading::class)], $t->getTraits()); } #[@test] public function trait_method_is_part_of_type() { $t= $this->type('class { use \lang\ast\unittest\emit\Loading; }'); - $this->assertTrue($t->hasMethod('loaded')); + Assert::true($t->hasMethod('loaded')); } #[@test] public function trait_is_resolved() { $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading; }'); - $this->assertEquals([new XPClass(Loading::class)], $t->getTraits()); + Assert::equals([new XPClass(Loading::class)], $t->getTraits()); } #[@test] @@ -35,7 +36,7 @@ public function trait_method_aliased() { loaded as hasLoaded; } }'); - $this->assertTrue($t->hasMethod('hasLoaded')); + Assert::true($t->hasMethod('hasLoaded')); } #[@test] @@ -45,6 +46,6 @@ public function trait_method_aliased_qualified() { Loading::loaded as hasLoaded; } }'); - $this->assertTrue($t->hasMethod('hasLoaded')); + Assert::true($t->hasMethod('hasLoaded')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 6d38e90b..241ef73e 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -1,11 +1,13 @@ transform('class', function($codegen, $class) { if ($class->annotation('repr')) { @@ -42,7 +44,7 @@ public function __construct(int $id) { $this->id= $id; } }'); - $this->assertFalse($t->hasMethod('id')); + Assert::false($t->hasMethod('id')); } #[@test] @@ -54,8 +56,8 @@ public function __construct(int $id) { $this->id= $id; } }'); - $this->assertTrue($t->hasMethod('toString')); - $this->assertEquals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); + Assert::true($t->hasMethod('toString')); + Assert::equals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); } #[@test, @values([['id', 1], ['name', 'Test']])] @@ -69,8 +71,8 @@ public function __construct(int $id, string $name) { $this->name= $name; } }'); - $this->assertTrue($t->hasMethod($name)); - $this->assertEquals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); + Assert::true($t->hasMethod($name)); + Assert::equals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); } #[@test] @@ -82,7 +84,7 @@ public function __construct(int $id) { $this->id= $id; } }'); - $this->assertEquals(1, $t->getMethod('id')->invoke($t->newInstance(1))); - $this->assertEquals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); + Assert::equals(1, $t->getMethod('id')->invoke($t->newInstance(1))); + Assert::equals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index a8a8cea6..e9ceab1a 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -2,13 +2,14 @@ use lang\XPClass; use lang\reflect\Modifiers; +use unittest\Assert; class TypeDeclarationTest extends EmittingTest { #[@test, @values(['class', 'interface', 'trait'])] public function empty_type($kind) { $t= $this->type($kind.' { }'); - $this->assertEquals( + Assert::equals( ['const' => [], 'fields' => [], 'methods' => []], ['const' => $t->getConstants(), 'fields' => $t->getFields(), 'methods' => $t->getMethods()] ); @@ -16,22 +17,22 @@ public function empty_type($kind) { #[@test] public function abstract_class_type() { - $this->assertTrue(Modifiers::isAbstract($this->type('abstract class { }')->getModifiers())); + Assert::true(Modifiers::isAbstract($this->type('abstract class { }')->getModifiers())); } #[@test] public function final_class_type() { - $this->assertTrue(Modifiers::isFinal($this->type('final class { }')->getModifiers())); + Assert::true(Modifiers::isFinal($this->type('final class { }')->getModifiers())); } #[@test] public function class_without_parent() { - $this->assertNull($this->type('class { }')->getParentclass()); + Assert::null($this->type('class { }')->getParentclass()); } #[@test] public function class_with_parent() { - $this->assertEquals( + Assert::equals( new XPClass(EmittingTest::class), $this->type('class extends \\lang\\ast\\unittest\\emit\\EmittingTest { }')->getParentclass() ); @@ -39,18 +40,18 @@ public function class_with_parent() { #[@test] public function trait_type() { - $this->assertTrue($this->type('trait { }')->isTrait()); + Assert::true($this->type('trait { }')->isTrait()); } #[@test] public function interface_type() { - $this->assertTrue($this->type('interface { }')->isInterface()); + Assert::true($this->type('interface { }')->isInterface()); } #[@test, @values(['public', 'private', 'protected'])] public function constant($modifiers) { $c= $this->type('class { '.$modifiers.' const test = 1; }')->getConstant('test'); - $this->assertEquals(1, $c); + Assert::equals(1, $c); } #[@test, @values([ @@ -60,7 +61,7 @@ public function constant($modifiers) { public function field($modifiers) { $f= $this->type('class { '.$modifiers.' $test; }')->getField('test'); $n= implode(' ', Modifiers::namesOf($f->getModifiers())); - $this->assertEquals( + Assert::equals( ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], ['name' => $f->getName(), 'type' => $f->getTypeName(), 'modifiers' => $n] ); @@ -74,7 +75,7 @@ public function field($modifiers) { public function method($modifiers) { $m= $this->type('class { '.$modifiers.' function test() { } }')->getMethod('test'); $n= implode(' ', Modifiers::namesOf($m->getModifiers())); - $this->assertEquals( + Assert::equals( ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], ['name' => $m->getName(), 'type' => $m->getReturnTypeName(), 'modifiers' => $n] ); @@ -83,7 +84,7 @@ public function method($modifiers) { #[@test] public function abstract_method() { $m= $this->type('abstract class { abstract function test(); }')->getMethod('test'); - $this->assertTrue(Modifiers::isAbstract($m->getModifiers())); + Assert::true(Modifiers::isAbstract($m->getModifiers())); } #[@test] @@ -105,6 +106,6 @@ public static function run($values) { return self::new($values)->forEach(function($a) { return $a * 2; }); } }'); - $this->assertEquals([2, 4, 6], $t->getMethod('run')->invoke(null, [[1, 2, 3]])); + Assert::equals([2, 4, 6], $t->getMethod('run')->invoke(null, [[1, 2, 3]])); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php index 184168a2..50b29757 100755 --- a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php @@ -1,5 +1,7 @@ assertEquals('mañana', $r); + Assert::equals('mañana', $r); } #[@test] @@ -21,6 +23,6 @@ public function run() { } }'); - $this->assertEquals('Smile! 😂', $r); + Assert::equals('Smile! 😂', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index 5d523223..b2ac363f 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -1,6 +1,7 @@ assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), $t->getField('test')->getType() ); @@ -27,7 +28,7 @@ public function parameter_type() { public function test(int|string $arg) { } }'); - $this->assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), $t->getMethod('test')->getParameter(0)->getType() ); @@ -39,7 +40,7 @@ public function return_type() { public function test(): int|string { } }'); - $this->assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), $t->getMethod('test')->getReturnType() ); diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index aac25bf5..b568f2cb 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -1,5 +1,6 @@ assertEquals(['read@1', '__dispose@1'], $r); + Assert::equals(['read@1', '__dispose@1'], $r); } #[@test] @@ -36,7 +37,7 @@ public function run() { return Handle::$called; } }'); - $this->assertEquals(['read@1', '__dispose@1', '__dispose@2'], $r); + Assert::equals(['read@1', '__dispose@1', '__dispose@2'], $r); } #[@test] @@ -56,7 +57,7 @@ public function run() { throw new IllegalStateException("No exception caught"); } }'); - $this->assertEquals(['read@1', '__dispose@1'], $r); + Assert::equals(['read@1', '__dispose@1'], $r); } #[@test] @@ -72,7 +73,7 @@ public function run() { return FileInput::$open; } }'); - $this->assertFalse($r); + Assert::false($r); } #[@test] @@ -90,7 +91,7 @@ public function run() { return ["called" => Handle::$called, "returned" => $returned]; } }'); - $this->assertEquals(['called' => ['read@1', '__dispose@1'], 'returned' => 'test'], $r); + Assert::equals(['called' => ['read@1', '__dispose@1'], 'returned' => 'test'], $r); } #[@test] @@ -103,7 +104,7 @@ public function run() { return isset($x); } }'); - $this->assertFalse($r); + Assert::false($r); } #[@test] @@ -117,6 +118,6 @@ public function run() { return isset($x); } }'); - $this->assertFalse($r); + Assert::false($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index cfdb4555..97d4cedb 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -1,5 +1,7 @@ assertEquals('Hello Test', $r); + Assert::equals('Hello Test', $r); } #[@test] @@ -25,6 +27,6 @@ public function run() { } }'); - $this->assertEquals(['Hello', 'Test'], $r); + Assert::equals(['Hello', 'Test'], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index 59f9dec9..00e64fa3 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -1,5 +1,6 @@ assertEquals([null, null], iterator_to_array($r)); + Assert::equals([null, null], iterator_to_array($r)); } #[@test] @@ -23,7 +24,7 @@ public function run() { yield 3; } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } #[@test] @@ -34,7 +35,7 @@ public function run() { yield "price" => 12.99; } }'); - $this->assertEquals(['color' => 'orange', 'price' => 12.99], iterator_to_array($r)); + Assert::equals(['color' => 'orange', 'price' => 12.99], iterator_to_array($r)); } #[@test] @@ -44,7 +45,7 @@ public function run() { yield from [1, 2, 3]; } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } #[@test] @@ -60,7 +61,7 @@ public function run() { yield from $this->values(); } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } #[@test] @@ -72,6 +73,6 @@ public function run() { yield 4; } }'); - $this->assertEquals([1, 2, 3, 4], iterator_to_array($r, false)); + Assert::equals([1, 2, 3, 4], iterator_to_array($r, false)); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 04ed0824..be5399d2 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -1,11 +1,12 @@ assertEquals('Tests', $this->load('Tests', 'getSimpleName()); + Assert::equals('Tests', $this->load('Tests', 'getSimpleName()); } #[@test, @expect([ @@ -63,7 +64,7 @@ public function trigger() { }'); $t->newInstance()->trigger(); - $this->assertNotEquals(false, strpos( + Assert::notEquals(false, strpos( preg_replace('#^.+://#', '', key(\xp::$errors)), strtr($t->getName(), '.', DIRECTORY_SEPARATOR).'.php' )); diff --git a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php index 673cf868..7d3e6550 100755 --- a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php @@ -1,6 +1,7 @@ returns= new ReturnStatement( new BinaryExpression( diff --git a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php index e4afae46..f610ce8f 100755 --- a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php @@ -1,6 +1,7 @@ return= new ReturnStatement(new Literal('null', self::LINE), self::LINE); } diff --git a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php index 00c8437a..2ff4c29d 100755 --- a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php @@ -1,11 +1,13 @@ blocks= [ 1 => [new InvokeExpression(new Literal('action1', self::LINE), [], self::LINE)], diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php index d58f89dd..c71e73e0 100755 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php @@ -1,6 +1,7 @@ fail('No exception raised', null, Errors::class); } catch (Errors $expected) { - $this->assertEquals($message, $expected->getMessage()); + Assert::equals($message, $expected->getMessage()); } } diff --git a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php index d28d9c1d..86cda762 100755 --- a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php @@ -1,7 +1,8 @@ expression= new BinaryExpression(new Variable('a', self::LINE), '+', new Literal('1', self::LINE), self::LINE); } diff --git a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php index 672fc828..8838f481 100755 --- a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php @@ -1,6 +1,7 @@ loop= new InvokeExpression(new Literal('loop', self::LINE), [], self::LINE); } diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index 72307027..d263d769 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -2,6 +2,7 @@ use lang\ast\Type; use lang\ast\nodes\{ClassDeclaration, Constant, InstanceExpression, InvokeExpression, Literal, Method, Property, ScopeExpression, Signature, Variable}; +use unittest\Assert; class MembersTest extends ParseTest { diff --git a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php index 01482191..531d9129 100755 --- a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php @@ -1,6 +1,7 @@ getName()))->execute(); + return (new Parse(Language::named('PHP'), new Tokens(new StringTokenizer($code)), static::class))->execute(); } /** @@ -31,6 +32,6 @@ protected function assertParsed($expected, $code) { foreach ($this->parse($code) as $node) { $actual[]= $node; } - $this->assertEquals($expected, $actual); + Assert::equals($expected, $actual); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php index e9b1d21e..e04c905c 100755 --- a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php @@ -1,6 +1,7 @@ Date: Sat, 30 Nov 2019 10:13:05 +0100 Subject: [PATCH 192/926] Merge #79 --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index a35db621..a62ffa81 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 5.0.0 / ????-??-?? +* Merged PR #79: Convert testsuite to baseless tests - @thekid * Merged PR #78: Deprecate curly brace syntax for offsets; consistent with PHP 7.4 (@thekid) From ed22e6779164daa0c2528d82ba8d770e7d78bfa2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 10:20:29 +0100 Subject: [PATCH 193/926] Merge #70 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a62ffa81..4e0eb3bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 5.0.0 / ????-??-?? +* Merged PR #70: Extract compact methods; to use these, require the + library https://github.com/xp-lang/php-compact-methods + (@thekid) * Merged PR #79: Convert testsuite to baseless tests - @thekid * Merged PR #78: Deprecate curly brace syntax for offsets; consistent with PHP 7.4 From 13c477f9bd869b8371279b1cba592d72620716f1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 10:36:32 +0100 Subject: [PATCH 194/926] Add compatibility with PHP7 oly versions of tokenize and ast libraries --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3348c154..a861eb27 100755 --- a/composer.json +++ b/composer.json @@ -7,8 +7,8 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/tokenize": "^8.1", - "xp-framework/ast": "^3.0", + "xp-framework/tokenize": "^9.0 | ^8.1", + "xp-framework/ast": "^4.0 | ^3.0", "php" : ">=7.0.0" }, "require-dev" : { From 46e5055f877ef1cb468c4219fe40e684cd68972c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 10:55:56 +0100 Subject: [PATCH 195/926] Make compatible with XP unittest v11.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a861eb27..dc9b39dd 100755 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/unittest": "^10.0" + "xp-framework/unittest": "^11.0 | ^10.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { From d3bdd89d58142304d66c3eadaf376d6166f5b940 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 30 Nov 2019 10:57:47 +0100 Subject: [PATCH 196/926] Prepare 5.0.0-RELEASE --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 4e0eb3bf..29c1c0f9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 5.0.0 / ????-??-?? +## 5.0.0 / 2019-11-30 * Merged PR #70: Extract compact methods; to use these, require the library https://github.com/xp-lang/php-compact-methods From c8169924175630516443931fac2b734884726bf7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 14:23:34 +0100 Subject: [PATCH 197/926] Allow `new ()` as syntax See php/php-src/pull#5061 --- src/main/php/lang/ast/emit/PHP.class.php | 15 ++++++++++--- src/main/php/lang/ast/syntax/PHP.class.php | 22 ++++++++++++++----- .../ast/unittest/parse/OperatorTest.class.php | 16 ++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b542850f..afc89534 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -662,9 +662,18 @@ protected function emitArguments($result, $arguments) { } protected function emitNew($result, $new) { - $result->out->write('new '.$new->type.'('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); + if ($new->type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'= '); + $this->emitOne($result, $new->type); + $result->out->write(') ? new '.$t.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(') : null'); + } else { + $result->out->write('new '.$new->type.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } } protected function emitNewClass($result, $new) { diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index ea704d7a..38f3fe2c 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -281,19 +281,29 @@ public function __construct() { }); $this->prefix('new', 0, function($parse, $token) { - $type= $parse->token; - $parse->forward(); + if ('(' === $parse->token->value) { + $parse->expecting('(', 'new type'); + $type= $this->expression($parse, 0); + $parse->expecting(')', 'new type'); + } else if ('variable' === $parse->token->kind) { + $type= '$'.$parse->token->value; + $parse->forward(); + } else if ('class' === $parse->token->value) { + $type= null; + $parse->forward(); + } else { + $type= $parse->scope->resolve($parse->token->value); + $parse->forward(); + } $parse->expecting('(', 'new arguments'); $arguments= $this->expressions($parse); $parse->expecting(')', 'new arguments'); - if ('variable' === $type->kind) { - return new NewExpression('$'.$type->value, $arguments, $token->line); - } else if ('class' === $type->value) { + if (null === $type) { return new NewClassExpression($this->clazz($parse, null), $arguments, $token->line); } else { - return new NewExpression($parse->scope->resolve($type->value), $arguments, $token->line); + return new NewExpression($type, $arguments, $token->line); } }); diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php index 81ce8177..c979722f 100755 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php @@ -157,6 +157,22 @@ public function new_type() { ); } + #[@test] + public function new_var() { + $this->assertParsed( + [new NewExpression('$class', [], self::LINE)], + 'new $class();' + ); + } + + #[@test] + public function new_expr() { + $this->assertParsed( + [new NewExpression(new InvokeExpression(new Literal('factory', self::LINE), [], self::LINE), [], self::LINE)], + 'new (factory())();' + ); + } + #[@test] public function new_type_with_args() { $this->assertParsed( From fe874ce7a3821f1b8a91ec01d7255bdcc37dc6bf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 14:26:39 +0100 Subject: [PATCH 198/926] Add test for instantiation operator --- .../unittest/emit/InstantiationTest.class.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php new file mode 100755 index 00000000..ce4af54d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php @@ -0,0 +1,54 @@ +run('class { + public function run() { + return new \\util\\Date(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[@test] + public function new_var() { + $r= $this->run('class { + public function run() { + $class= \\util\\Date::class; + return new $class(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[@test] + public function new_expr() { + $r= $this->run('class { + private function factory() { return \\util\\Date::class; } + + public function run() { + return new ($this->factory())(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[@test] + public function passing_argument() { + $r= $this->run('class { + public $value; + + public function __construct($value= null) { $this->value= $value; } + + public function run() { + return new self("Test"); + } + }'); + Assert::equals('Test', $r->value); + } +} \ No newline at end of file From ff2d8c99deaf1667b01de2fcf6403092a57f664f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 14:29:35 +0100 Subject: [PATCH 199/926] QA: Use [] for string offsets instead of {}, which is deprecated --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index afc89534..7c274d95 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -122,7 +122,7 @@ protected function emitCast($result, $cast) { static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; $name= $cast->type->name(); - if ('?' === $name{0}) { + if ('?' === $name[0]) { $result->out->write('cast('); $this->emitOne($result, $cast->expression); $result->out->write(',\''.$name.'\', false)'); From 651f963877a54a83b4f7b6eb2785f7cf7870a26c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 14:38:06 +0100 Subject: [PATCH 200/926] Document `new ()` syntax support Forward compatibility with PHP 8.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 29c1c0f9..a99657f2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #80: Allow `new ()` as syntax - @thekid + ## 5.0.0 / 2019-11-30 * Merged PR #70: Extract compact methods; to use these, require the From 548fe2e2d8f0cbc145e3ccc0664e6522aa43bafd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 15:01:48 +0100 Subject: [PATCH 201/926] Add native support for arbitrary expressions with new --- src/main/php/lang/ast/emit/PHP80.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 342baa62..f2f74f9f 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,5 +1,7 @@ null, ]; + + protected function emitNew($result, $new) { + if ($new->type instanceof Node) { + $result->out->write('new ('); + $this->emitOne($result, $new->type); + $result->out->write(')('); + } else { + $result->out->write('new '.$new->type.'('); + } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } } \ No newline at end of file From 662c3b9b8ab16b91f568e32fd68a464b0f51514a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 15:03:50 +0100 Subject: [PATCH 202/926] Use PHP 7.4 in test matrix instead of 7.4snapshot (they are the same) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b10f30dc..602f0d76 100755 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ php: - 7.1 - 7.2 - 7.3 - - 7.4snapshot + - 7.4 - nightly matrix: From 6dadad35201634648899d6e09aee02736df961c1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 19:23:27 +0100 Subject: [PATCH 203/926] Verify class constants --- .../lang/ast/unittest/emit/MembersTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 74612ee9..cfd797a9 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -30,6 +30,19 @@ public function run() { Assert::equals('Test', $r); } + #[@test] + public function class_constant() { + $r= $this->run('class { + private const MEMBER = "Test"; + + public function run() { + return self::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + #[@test] public function instance_property() { $r= $this->run('class { From 5e6212150699c0593466b91cd8922328a32305e9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 20:05:20 +0100 Subject: [PATCH 204/926] Allow `instanceof ()` as syntax See php/php-src#5061 --- src/main/php/lang/ast/emit/PHP.class.php | 20 ++++++++-- src/main/php/lang/ast/syntax/PHP.class.php | 11 ++++-- .../unittest/emit/InstanceOfTest.class.php | 39 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7c274d95..cdbc9b0c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,5 +1,6 @@ emitOne($result, $instanceof->expression); - $result->out->write(' instanceof '); - if ($instanceof->type instanceof Node) { + + // Supported: instanceof T, instanceof $t, instanceof $t DEREF; instanceof T::(NAME | VARIABLE) + // Unsupported: instanceof EXPR + if ($instanceof->type instanceof Variable || $instanceof->type instanceof InstanceExpression || $instanceof->type instanceof ScopeExpression) { + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '); + $this->emitOne($result, $instanceof->type); + } else if ($instanceof->type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'= '); $this->emitOne($result, $instanceof->type); + $result->out->write(')?'); + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '.$t.':null'); } else { - $result->out->write($instanceof->type); + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '.$instanceof->type); } } diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 38f3fe2c..4971a4e6 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -116,13 +116,16 @@ public function __construct() { $this->infixr('>>', 70); $this->infix('instanceof', 110, function($parse, $token, $left) { + + // Solve ambiguity EXPR instanceof T vs. EXPR instanceof T::MEMBER by look-ahead if ('name' === $parse->token->kind) { - $type= $parse->scope->resolve($parse->token->value); + $name= $parse->token; $parse->forward(); - return new InstanceOfExpression($left, $type, $token->line); - } else { - return new InstanceOfExpression($left, $this->expression($parse, 0), $token->line); + if ('::' !== $parse->token->value) return new InstanceOfExpression($left, $parse->scope->resolve($name->value), $token->line); + $parse->queue[]= $parse->token; + $parse->token= $name; } + return new InstanceOfExpression($left, $this->expression($parse, 0), $token->line); }); $this->infix('->', 100, function($parse, $token, $left) { diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index 004ddbda..374f1b94 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -58,4 +58,43 @@ public function run() { Assert::true($r); } + + #[@test] + public function instanceof_instance_expr() { + $r= $this->run('class { + private $type= self::class; + + public function run() { + return $this instanceof $this->type; + } + }'); + + Assert::true($r); + } + + #[@test] + public function instanceof_scope_expr() { + $r= $this->run('class { + private static $type= self::class; + + public function run() { + return $this instanceof self::$type; + } + }'); + + Assert::true($r); + } + + #[@test] + public function instanceof_expr() { + $r= $this->run('class { + private function type() { return self::class; } + + public function run() { + return $this instanceof ($this->type()); + } + }'); + + Assert::true($r); + } } \ No newline at end of file From d653d3994cabaa82719cb542b9a287918b801220 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 20:11:09 +0100 Subject: [PATCH 205/926] QA: Shorten code --- src/main/php/lang/ast/emit/PHP.class.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index cdbc9b0c..c46de76d 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -645,23 +645,24 @@ protected function emitGoto($result, $goto) { } protected function emitInstanceOf($result, $instanceof) { + $type= $instanceof->type; - // Supported: instanceof T, instanceof $t, instanceof $t DEREF; instanceof T::(NAME | VARIABLE) + // Supported: instanceof T, instanceof $t, instanceof $t->MEMBER; instanceof T::MEMBER // Unsupported: instanceof EXPR - if ($instanceof->type instanceof Variable || $instanceof->type instanceof InstanceExpression || $instanceof->type instanceof ScopeExpression) { + if ($type instanceof Variable || $type instanceof InstanceExpression || $type instanceof ScopeExpression) { $this->emitOne($result, $instanceof->expression); $result->out->write(' instanceof '); - $this->emitOne($result, $instanceof->type); - } else if ($instanceof->type instanceof Node) { + $this->emitOne($result, $type); + } else if ($type instanceof Node) { $t= $result->temp(); $result->out->write('('.$t.'= '); - $this->emitOne($result, $instanceof->type); + $this->emitOne($result, $type); $result->out->write(')?'); $this->emitOne($result, $instanceof->expression); $result->out->write(' instanceof '.$t.':null'); } else { $this->emitOne($result, $instanceof->expression); - $result->out->write(' instanceof '.$instanceof->type); + $result->out->write(' instanceof '.$type); } } From c33f3a3ec79529cce45caf5e24e04000b4af2a80 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 21:11:04 +0100 Subject: [PATCH 206/926] Simplify code when parsing `instanceof` --- src/main/php/lang/ast/syntax/PHP.class.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 4971a4e6..292f2b55 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -116,16 +116,12 @@ public function __construct() { $this->infixr('>>', 70); $this->infix('instanceof', 110, function($parse, $token, $left) { - - // Solve ambiguity EXPR instanceof T vs. EXPR instanceof T::MEMBER by look-ahead - if ('name' === $parse->token->kind) { - $name= $parse->token; - $parse->forward(); - if ('::' !== $parse->token->value) return new InstanceOfExpression($left, $parse->scope->resolve($name->value), $token->line); - $parse->queue[]= $parse->token; - $parse->token= $name; + $type= $this->expression($parse, 0); + if ($type instanceof Literal) { + return new InstanceOfExpression($left, $parse->scope->resolve($type->expression), $token->line); + } else { + return new InstanceOfExpression($left, $type, $token->line); } - return new InstanceOfExpression($left, $this->expression($parse, 0), $token->line); }); $this->infix('->', 100, function($parse, $token, $left) { From 420cd512063119f7849da6e6655cf51d0a2ebe94 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Feb 2020 21:15:56 +0100 Subject: [PATCH 207/926] Document `instanceof ()` syntax support Forward compatibility with PHP 8.0 --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index a99657f2..fa8ee2b0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #81: Allow `instanceof ()` as syntax - @thekid * Merged PR #80: Allow `new ()` as syntax - @thekid ## 5.0.0 / 2019-11-30 From 69ae2c988b15a3018b55e2195511089d6796dc6b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Feb 2020 00:16:42 +0100 Subject: [PATCH 208/926] Allow chaining scope resolution operator `::` --- src/main/php/lang/ast/emit/PHP.class.php | 17 ++++- src/main/php/lang/ast/syntax/PHP.class.php | 9 ++- .../ast/unittest/emit/MembersTest.class.php | 62 +++++++++++++++++++ .../ast/unittest/parse/MembersTest.class.php | 6 +- 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index c46de76d..9a2d97fc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -717,8 +717,21 @@ protected function emitInvoke($result, $invoke) { } protected function emitScope($result, $scope) { - $result->out->write($scope->type.'::'); - $this->emitOne($result, $scope->member); + if ($scope->type instanceof Variable) { + $this->emitOne($result, $scope->type); + $result->out->write('::'); + $this->emitOne($result, $scope->member); + } else if ($scope->type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'='); + $this->emitOne($result, $scope->type); + $result->out->write(')?'.$t.'::'); + $this->emitOne($result, $scope->member); + $result->out->write(':null'); + } else { + $result->out->write($scope->type.'::'); + $this->emitOne($result, $scope->member); + } } protected function emitInstance($result, $instance) { diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 292f2b55..c65d2201 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -138,7 +138,7 @@ public function __construct() { }); $this->infix('::', 100, function($parse, $token, $left) { - $scope= $parse->scope->resolve($left->expression); + $scope= $left instanceof Literal ? $parse->scope->resolve($left->expression) : $left; if ('variable' === $parse->token->kind) { $expr= new Variable($parse->token->value, $parse->token->line); @@ -150,6 +150,13 @@ public function __construct() { } $parse->forward(); + if ('(' === $parse->token->value) { + $parse->expecting('(', 'invoke expression'); + $arguments= $this->expressions($parse); + $parse->expecting(')', 'invoke expression'); + $expr= new InvokeExpression($expr, $arguments, $token->line); + } + return new ScopeExpression($scope, $expr, $token->line); }); diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index cfd797a9..e469c3d3 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -43,6 +43,48 @@ public function run() { Assert::equals('Test', $r); } + #[@test] + public function dynamic_class_property() { + $r= $this->run('class { + private static $MEMBER= "Test"; + + public function run() { + $class= self::class; + return $class::$MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[@test] + public function dynamic_class_method() { + $r= $this->run('class { + private static function member() { return "Test"; } + + public function run() { + $class= self::class; + return $class::member(); + } + }'); + + Assert::equals('Test', $r); + } + + #[@test] + public function dynamic_class_constant() { + $r= $this->run('class { + private const MEMBER = "Test"; + + public function run() { + $class= self::class; + return $class::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + #[@test] public function instance_property() { $r= $this->run('class { @@ -122,4 +164,24 @@ public function run() { Assert::null($r); } + + #[@test] + public function chaining_sccope_operators() { + $r= $this->run('class { + private const TYPE = self::class; + + private const NAME = "Test"; + + private static $name = "Test"; + + private static function name() { return "Test"; } + + public function run() { + $name= "name"; + return [self::TYPE::NAME, self::TYPE::$name, self::TYPE::name(), self::TYPE::$name()]; + } + }'); + + Assert::equals(['Test', 'Test', 'Test', 'Test'], $r); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php index d263d769..44f4b0c0 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -188,9 +188,9 @@ public function instance_method_invocation() { #[@test] public function static_method_invocation() { $this->assertParsed( - [new InvokeExpression( - new ScopeExpression('\\A', new Literal('member', self::LINE), self::LINE), - [new Literal('1', self::LINE)], + [new ScopeExpression( + '\\A', + new InvokeExpression(new Literal('member', self::LINE), [new Literal('1', self::LINE)], self::LINE), self::LINE )], 'A::member(1);' From c4a6ae59e220494f05113f73a11dc3a800ff73cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Mar 2020 16:15:40 +0100 Subject: [PATCH 209/926] Prepare 5.1.0 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index fa8ee2b0..c8e5678e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.1.0 / 2020-03-28 + +* Merged PR #82: Allow chaining scope resolution operator `::` - @thekid * Merged PR #81: Allow `instanceof ()` as syntax - @thekid * Merged PR #80: Allow `new ()` as syntax - @thekid From 85d86f97cb491fef3293dcea6b9e18017801389a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Mar 2020 13:37:26 +0200 Subject: [PATCH 210/926] Verify trailing commas are allowed in parameter lists See https://wiki.php.net/rfc/trailing_comma_in_parameter_list --- .../php/lang/ast/unittest/emit/ParameterTest.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 94612827..eb0b912c 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -110,4 +110,13 @@ public function optional_parameter() { public function optional_parameters_default_value() { Assert::equals(true, $this->param('$param= true')->getDefaultValue()); } + + #[@test] + public function trailing_comma_allowed() { + $p= $this->type('class { public function fixture($param, ) { } }') + ->getMethod('fixture') + ->getParameters() + ; + Assert::equals(1, sizeof($p), 'number of parameters'); + } } \ No newline at end of file From d783084290de42f839f23fded981a36c722f6880 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Mar 2020 18:18:14 +0200 Subject: [PATCH 211/926] Fix ternary and instanceof operators' precedence --- ChangeLog.md | 4 ++++ src/main/php/lang/ast/syntax/PHP.class.php | 4 ++-- .../lang/ast/unittest/emit/TernaryTest.class.php | 13 +++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c8e5678e..c190850c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.1.1 / 2020-03-29 + +* Fixed ternary and instanceof operators' precedence - @thekid + ## 5.1.0 / 2020-03-28 * Merged PR #82: Allow chaining scope resolution operator `::` - @thekid diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index c65d2201..ec7cbb97 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -116,7 +116,7 @@ public function __construct() { $this->infixr('>>', 70); $this->infix('instanceof', 110, function($parse, $token, $left) { - $type= $this->expression($parse, 0); + $type= $this->expression($parse, 110); if ($type instanceof Literal) { return new InstanceOfExpression($left, $parse->scope->resolve($type->expression), $token->line); } else { @@ -137,7 +137,7 @@ public function __construct() { return new InstanceExpression($left, $expr, $token->line); }); - $this->infix('::', 100, function($parse, $token, $left) { + $this->infix('::', 120, function($parse, $token, $left) { $scope= $left instanceof Literal ? $parse->scope->resolve($left->expression) : $left; if ('variable' === $parse->token->kind) { diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 3c900034..09b7bc45 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -1,5 +1,6 @@ run( + 'class { + public function run($value) { + return $value instanceof \\io\\Path ? $value : new \\io\\Path($value); + } + }', + $value + )); + } } \ No newline at end of file From 73312569d05c87c65b9f23513a1bd79ce17b5e30 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 15:51:05 +0200 Subject: [PATCH 212/926] Update state of Constructor Property Promotion RFC It is being discussed as candidate for PHP 8.0 --- .../php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index d0bd5400..dcbe4b39 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -8,7 +8,7 @@ * * @see https://github.com/xp-framework/rfc/issues/240 * @see https://docs.hhvm.com/hack/other-features/constructor-parameter-promotion - * @see https://wiki.php.net/rfc/constructor-promotion (Draft) + * @see https://wiki.php.net/rfc/constructor-promotion (Under Discussion for PHP 8.0) * @see https://wiki.php.net/rfc/automatic_property_initialization (Declined) */ class ArgumentPromotionTest extends EmittingTest { From 9c26f62a3e65aed8de97eaf7db3f3e04d0a188fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 16:00:45 +0200 Subject: [PATCH 213/926] Raise errors when promoting variadic parameters Consistency with Constructor Property Promotion RFC See https://wiki.php.net/rfc/constructor_promotion#constraints --- src/main/php/lang/ast/syntax/PHP.class.php | 8 ++++++-- .../ast/unittest/emit/ArgumentPromotionTest.class.php | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index ec7cbb97..77eff362 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -1138,11 +1138,15 @@ private function parameters($parse, $target) { $type= $this->type($parse); - if ('...' === $parse->token->value) { + if ('...' !== $parse->token->value) { + $variadic= false; + } else if ($promote) { + $parse->raise('Variadic parameters cannot be promoted', 'parameters'); $variadic= true; $parse->forward(); } else { - $variadic= false; + $variadic= true; + $parse->forward(); } if ('&' === $parse->token->value) { diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index dcbe4b39..e5062dfd 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -1,6 +1,7 @@ getField('id')->getType(), $t->getField('name')->getType()] ); } + + #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Variadic parameters cannot be promoted'])] + public function variadic_parameters_cannot_be_promoted() { + $this->type('class { + public function __construct(private string... $in) { } + }'); + } } \ No newline at end of file From b959e97254ce24444c15413c7e5bc65ea82823ac Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 16:08:17 +0200 Subject: [PATCH 214/926] Verify mixing promoted and normal arguments works --- .../ast/unittest/emit/ArgumentPromotionTest.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index e5062dfd..c0fb8395 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -74,4 +74,15 @@ public function variadic_parameters_cannot_be_promoted() { public function __construct(private string... $in) { } }'); } + + #[@test] + public function can_be_mixed_with_normal_arguments() { + $t= $this->type('class { + public function __construct(public string $name, string $initial= null) { + if (null !== $initial) $this->name.= " ".$initial."."; + } + }'); + Assert::equals(['name'], array_map(function($f) { return $f->getName(); }, $t->getFields())); + Assert::equals('Timm J.', $t->newInstance('Timm', 'J')->name); + } } \ No newline at end of file From 285086ed6762bcc5361f8dae8d3fb5c129c719f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 16:22:20 +0200 Subject: [PATCH 215/926] Fix promotion for by-reference arguments --- ChangeLog.md | 4 +++ src/main/php/lang/ast/emit/PHP.class.php | 2 +- .../emit/ArgumentPromotionTest.class.php | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index c190850c..5e61efb2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.1.2 / 2020-04-04 + +* Fixed promotion for by-reference arguments - @thekid + ## 5.1.1 / 2020-03-29 * Fixed ternary and instanceof operators' precedence - @thekid diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9a2d97fc..d96e99e4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -406,7 +406,7 @@ protected function emitMethod($result, $method) { foreach ($method->signature->parameters as $param) { if (isset($param->promote)) { $declare.= $param->promote.' $'.$param->name.';'; - $promote.= '$this->'.$param->name.'= $'.$param->name.';'; + $promote.= '$this->'.$param->name.'= '.($param->reference ? '&$' : '$').$param->name.';'; $result->meta[0][self::PROPERTY][$param->name]= [ DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', DETAIL_ANNOTATIONS => [], diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index c0fb8395..07bee4aa 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -42,6 +42,22 @@ public function run() { Assert::equals('tested', $r); } + #[@test] + public function parameter_accessible() { + $r= $this->run('class { + public function __construct(private $id= "test") { + if (null === $id) { + throw new \\lang\\IllegalArgumentException("ID not set"); + } + } + + public function run() { + return $this->id; + } + }'); + Assert::equals('test', $r); + } + #[@test] public function in_method() { $r= $this->run('class { @@ -85,4 +101,20 @@ public function __construct(public string $name, string $initial= null) { Assert::equals(['name'], array_map(function($f) { return $f->getName(); }, $t->getFields())); Assert::equals('Timm J.', $t->newInstance('Timm', 'J')->name); } + + #[@test] + public function promoted_by_reference_argument() { + $t= $this->type('class { + public function __construct(public array &$list) { } + + public static function test() { + $list= [1, 2, 3]; + $self= new self($list); + $list[]= 4; + return $self->list; + } + }'); + + Assert::equals([1, 2, 3, 4], $t->getMethod('test')->invoke(null, [])); + } } \ No newline at end of file From 63c97e7b4e2669a5b9dd1c1f3df0123686a06002 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 16:31:54 +0200 Subject: [PATCH 216/926] Verify trailing commas are allowed See https://wiki.php.net/rfc/constructor_promotion#future_scope --- .../ast/unittest/emit/ArgumentPromotionTest.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 07bee4aa..36c571d4 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -117,4 +117,15 @@ public static function test() { Assert::equals([1, 2, 3, 4], $t->getMethod('test')->invoke(null, [])); } + + #[@test] + public function allows_trailing_comma() { + $this->type('class { + public function __construct( + public float $x = 0.0, + public float $y = 0.0, + public float $z = 0.0, // <-- Allow this comma. + ) { } + }'); + } } \ No newline at end of file From 6c385132b9a579f7c605fc4e55ccae3d7a01da0b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 17:39:22 +0200 Subject: [PATCH 217/926] Allow ::class on objects See https://wiki.php.net/rfc/class_name_literal_on_object --- ChangeLog.md | 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 +- .../ast/unittest/emit/MembersTest.class.php | 24 +++++++++++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 5e61efb2..14f9547f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Allowed `::class` on objects (PHP 8.0 forward compatibility) - @thekid + ## 5.1.2 / 2020-04-04 * Fixed promotion for by-reference arguments - @thekid diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 2efd10fb..e29bd7f4 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -7,7 +7,7 @@ */ class PHP70 extends PHP { use OmitPropertyTypes, OmitConstModifiers; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects; protected $unsupported= [ 'object' => 72, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 4e7ba213..14c039a1 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -7,7 +7,7 @@ */ class PHP71 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects; protected $unsupported= [ 'object' => 72, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 18e55120..bfe5593d 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -7,7 +7,7 @@ */ class PHP72 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects; protected $unsupported= [ 'mixed' => null, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 91a20cc0..d31030ec 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -6,7 +6,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects; protected $unsupported= [ 'mixed' => null, diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index e469c3d3..c1766936 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -85,6 +85,30 @@ public function run() { Assert::equals('Test', $r); } + #[@test] + public function object_class_constant() { + $r= $this->run('class { + private const MEMBER = "Test"; + + public function run() { + return $this::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[@test] + public function this_class_constant() { + $r= $this->run('class { + public function run() { + return [get_class($this), $this::class]; + } + }'); + + Assert::equals($r[0], $r[1]); + } + #[@test] public function instance_property() { $r= $this->run('class { From f28d0838cfd297ddf596dcb3bfe1c7d3c8a92429 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 17:40:08 +0200 Subject: [PATCH 218/926] Rewrite ::class on objects to get_class() --- .../ast/emit/RewriteClassOnObjects.class.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php new file mode 100755 index 00000000..e138f09e --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php @@ -0,0 +1,21 @@ +type instanceof Variable && $scope->member instanceof Literal && 'class' === $scope->member->expression) { + $result->out->write('get_class('); + $this->emitOne($result, $scope->type); + $result->out->write(')'); + } else { + parent::emitScope($result, $scope); + } + } +} \ No newline at end of file From e129bc2a040efd72f7417a1f0e1ef2cd46b03e11 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 17:51:42 +0200 Subject: [PATCH 219/926] Verify further dynamic object access methods --- .../ast/emit/RewriteClassOnObjects.class.php | 8 +++++--- .../ast/unittest/emit/MembersTest.class.php | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php index e138f09e..3c49a57c 100755 --- a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php +++ b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php @@ -1,16 +1,18 @@ type instanceof Variable && $scope->member instanceof Literal && 'class' === $scope->member->expression) { + if ($scope->member instanceof Literal && 'class' === $scope->member->expression && !is_string($scope->type)) { $result->out->write('get_class('); $this->emitOne($result, $scope->type); $result->out->write(')'); diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index c1766936..260e2cef 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -98,15 +98,20 @@ public function run() { Assert::equals('Test', $r); } - #[@test] - public function this_class_constant() { - $r= $this->run('class { - public function run() { - return [get_class($this), $this::class]; - } + #[@test, @values(['variable', 'invocation', 'array'])] + public function class_on_objects($via) { + $t= $this->type('class { + private function this() { return $this; } + + public function variable() { return $this::class; } + + public function invocation() { return $this->this()::class; } + + public function array() { return [$this][0]::class; } }'); - Assert::equals($r[0], $r[1]); + $fixture= $t->newInstance(); + Assert::equals(get_class($fixture), $t->getMethod($via)->invoke($fixture)); } #[@test] From ab113cafbd0210d9100ec4a41b5d8ae0c07229c7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 17:52:15 +0200 Subject: [PATCH 220/926] Remove unused import --- src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php index 3c49a57c..cd87a40d 100755 --- a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php +++ b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php @@ -1,6 +1,5 @@ Date: Sat, 4 Apr 2020 17:52:49 +0200 Subject: [PATCH 221/926] Fully qualify get_class() function name --- src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php index cd87a40d..9a897ea6 100755 --- a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php +++ b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php @@ -12,7 +12,7 @@ trait RewriteClassOnObjects { protected function emitScope($result, $scope) { if ($scope->member instanceof Literal && 'class' === $scope->member->expression && !is_string($scope->type)) { - $result->out->write('get_class('); + $result->out->write('\\get_class('); $this->emitOne($result, $scope->type); $result->out->write(')'); } else { From 3599f0ea292ebcddae986fadf8529e800c25ea78 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 4 Apr 2020 17:54:32 +0200 Subject: [PATCH 222/926] Release 5.1.3 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 14f9547f..ba7bcc3c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.1.3 / 2020-04-04 + * Allowed `::class` on objects (PHP 8.0 forward compatibility) - @thekid ## 5.1.2 / 2020-04-04 From 5c7ab96b53882fdb41242d010f30e0d654863ae5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Apr 2020 17:20:56 +0200 Subject: [PATCH 223/926] Verify binary && and || work with throw expressions See https://wiki.php.net/rfc/throw_expression (in voting for PHP 8.0) --- .../unittest/emit/ExceptionsTest.class.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index ad53c3a3..4ff68434 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -120,6 +120,26 @@ public function run($user) { $t->newInstance()->run(null); } + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_binary_or() { + $t= $this->type('class { + public function run($user) { + return $user || throw new \\lang\\IllegalArgumentException("test"); + } + }'); + $t->newInstance()->run(null); + } + + #[@test, @expect(IllegalArgumentException::class)] + public function throw_expression_with_binary_and() { + $t= $this->type('class { + public function run($error) { + return $error && throw new \\lang\\IllegalArgumentException("test"); + } + }'); + $t->newInstance()->run(true); + } + #[@test, @expect(IllegalArgumentException::class)] public function throw_expression_with_lambda() { $this->run('use lang\IllegalArgumentException; class { From 8993a432bc346b144529795a68f5807a05cb00a3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 15:59:42 +0200 Subject: [PATCH 224/926] Extract parser to xp-framework/ast --- src/main/php/lang/ast/Error.class.php | 17 - src/main/php/lang/ast/Errors.class.php | 44 - src/main/php/lang/ast/Language.class.php | 165 --- src/main/php/lang/ast/Parse.class.php | 147 -- src/main/php/lang/ast/Scope.class.php | 79 - src/main/php/lang/ast/Symbol.class.php | 27 - src/main/php/lang/ast/Token.class.php | 43 - src/main/php/lang/ast/Tokens.class.php | 136 -- .../php/lang/ast/syntax/Extension.class.php | 13 - src/main/php/lang/ast/syntax/PHP.class.php | 1308 ----------------- .../ast/syntax/TransformationApi.class.php | 12 - .../ast/unittest/parse/BlocksTest.class.php | 23 - .../ast/unittest/parse/ClosuresTest.class.php | 72 - .../ast/unittest/parse/CommentTest.class.php | 87 -- .../unittest/parse/ConditionalTest.class.php | 87 -- .../ast/unittest/parse/ErrorsTest.class.php | 87 -- .../unittest/parse/FunctionsTest.class.php | 160 -- .../ast/unittest/parse/InvokeTest.class.php | 56 - .../ast/unittest/parse/LambdasTest.class.php | 45 - .../ast/unittest/parse/LiteralsTest.class.php | 84 -- .../ast/unittest/parse/LoopsTest.class.php | 158 -- .../ast/unittest/parse/MembersTest.class.php | 258 ---- .../unittest/parse/NamespacesTest.class.php | 55 - .../ast/unittest/parse/OperatorTest.class.php | 313 ---- .../ast/unittest/parse/ParseTest.class.php | 37 - .../unittest/parse/StartTokensTest.class.php | 23 - .../ast/unittest/parse/TypesTest.class.php | 137 -- .../unittest/parse/VariablesTest.class.php | 49 - 28 files changed, 3722 deletions(-) delete mode 100755 src/main/php/lang/ast/Error.class.php delete mode 100755 src/main/php/lang/ast/Errors.class.php delete mode 100755 src/main/php/lang/ast/Language.class.php delete mode 100755 src/main/php/lang/ast/Parse.class.php delete mode 100755 src/main/php/lang/ast/Scope.class.php delete mode 100755 src/main/php/lang/ast/Symbol.class.php delete mode 100755 src/main/php/lang/ast/Token.class.php delete mode 100755 src/main/php/lang/ast/Tokens.class.php delete mode 100755 src/main/php/lang/ast/syntax/Extension.class.php delete mode 100755 src/main/php/lang/ast/syntax/PHP.class.php delete mode 100755 src/main/php/lang/ast/syntax/TransformationApi.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/BlocksTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/CommentTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/InvokeTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/LambdasTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/LoopsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/MembersTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/OperatorTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/ParseTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/TypesTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/parse/VariablesTest.class.php diff --git a/src/main/php/lang/ast/Error.class.php b/src/main/php/lang/ast/Error.class.php deleted file mode 100755 index 10f9a0a8..00000000 --- a/src/main/php/lang/ast/Error.class.php +++ /dev/null @@ -1,17 +0,0 @@ -file= $file; - $this->line= $line; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Errors.class.php b/src/main/php/lang/ast/Errors.class.php deleted file mode 100755 index e8ab86dc..00000000 --- a/src/main/php/lang/ast/Errors.class.php +++ /dev/null @@ -1,44 +0,0 @@ -getMessage(); - break; - - default: - $message= "Errors {\n"; - foreach ($errors as $error) { - $message.= ' '.$error->message.' at line '.$error->line."\n"; - } - $message.= '}'; - } - - parent::__construct($message); - $this->file= $file; - $this->line= $errors[0]->getLine(); - } - - /** - * Returns diagnostics - * - * @param string $indent - * @return string - */ - public function diagnostics($indent= '') { - return str_replace("\n", "\n".$indent, $this->message); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Language.class.php b/src/main/php/lang/ast/Language.class.php deleted file mode 100755 index 2389bbb3..00000000 --- a/src/main/php/lang/ast/Language.class.php +++ /dev/null @@ -1,165 +0,0 @@ -symbols[$id])) { - $symbol= $this->symbols[$id]; - if ($lbp > $symbol->lbp) { - $symbol->lbp= $lbp; - } - } else { - $symbol= new Symbol(); - $symbol->id= $id; - $symbol->lbp= $lbp; - $this->symbols[$id]= $symbol; - } - return $symbol; - } - - public function constant($id, $value) { - $const= $this->symbol($id); - $const->nud= function($parse, $token) use($value) { - return new Literal($value, $token->line); - }; - } - - public function assignment($id) { - $infix= $this->symbol($id, 10); - $infix->led= function($parse, $token, $left) use($id) { - return new Assignment($left, $id, $this->expression($parse, 9), $token->line); - }; - } - - public function infix($id, $bp, $led= null) { - $infix= $this->symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expression($parse, $bp), $token->line); - }; - } - - public function infixr($id, $bp, $led= null) { - $infix= $this->symbol($id, $bp); - $infix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id, $bp) { - return new BinaryExpression($left, $id, $this->expression($parse, $bp - 1), $token->line); - }; - } - - public function prefix($id, $bp, $nud= null) { - $prefix= $this->symbol($id); - $prefix->nud= $nud ? $nud->bindTo($this, static::class) : function($parse, $token) use($id, $bp) { - $expr= $this->expression($parse, $bp); - return new UnaryExpression('prefix', $expr, $id, $token->line); - }; - } - - public function suffix($id, $bp, $led= null) { - $suffix= $this->symbol($id, $bp); - $suffix->led= $led ? $led->bindTo($this, static::class) : function($parse, $token, $left) use($id) { - $expr= new UnaryExpression('suffix', $left, $id, $token->line); - return $expr; - }; - } - - public function stmt($id, $func) { - $stmt= $this->symbol($id); - $stmt->std= $func->bindTo($this, static::class); - } - - /** - * Returns a single expression - * - * @param lang.ast.Parse $parse - * @param int $rbp - * @return lang.ast.Node - */ - public function expression($parse, $rbp) { - $t= $parse->token; - $parse->forward(); - $left= $t->symbol->nud ? $t->symbol->nud->__invoke($parse, $t) : $t; - - while ($rbp < $parse->token->symbol->lbp) { - $t= $parse->token; - $parse->forward(); - $left= $t->symbol->led ? $t->symbol->led->__invoke($parse, $t, $left) : $t; - } - - return $left; - } - - /** - * Returns a single statement - * - * @param lang.ast.Parse $parse - * @return lang.ast.Node - */ - public function statement($parse) { - if ($parse->token->symbol->std) { - $t= $parse->token; - $parse->forward(); - return $t->symbol->std->__invoke($parse, $t); - } - - $expr= $this->expression($parse, 0); - - // Check for semicolon - if (';' !== $parse->token->symbol->id) { - $parse->raise('Missing semicolon after '.$expr->kind.' statement', null, $expr->line); - } else { - $parse->forward(); - } - - return $expr; - } - - /** - * Returns a list of statements - * - * @param lang.ast.Parse $parse - * @return lang.ast.Node[] - */ - public function statements($parse) { - $statements= []; - while ('}' !== $parse->token->value) { - if (null === ($statement= $this->statement($parse))) break; - $statements[]= $statement; - } - return $statements; - } - - /** - * Returns extensions for this language. By convention, these are loaded - * from a package with the same name as the class (but in lowercase). - * - * @return iterable - */ - public function extensions() { - yield new TransformationApi(); - foreach (Package::forName(strtr(strtolower(static::class), '\\', '.'))->getClasses() as $class) { - if ($class->isSubclassOf(Extension::class)) yield $class->newInstance(); - } - } - - /** - * Returns a language with the given name - * - * @param string $name - * @return self - */ - public static function named($name) { - if (!isset(self::$instance[$name])) { - self::$instance[$name]= Package::forName('lang.ast.syntax')->loadClass($name)->newInstance(); - } - return self::$instance[$name]; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php deleted file mode 100755 index c23c3151..00000000 --- a/src/main/php/lang/ast/Parse.class.php +++ /dev/null @@ -1,147 +0,0 @@ -language= $language; - $this->tokens= $tokens->getIterator(); - $this->file= $file; - $this->scope= new Scope(null); - } - - /** - * Raise an error - * - * @param string $error - * @param string $context - * @param int $line - * @return void - */ - public function raise($message, $context= null, $line= null) { - $context && $message.= ' in '.$context; - $this->errors[]= new Error($message, $this->file, $line ?: $this->token->line); - } - - /** - * Emit a warning - * - * @param string $error - * @param string $context - * @return void - */ - public function warn($message, $context= null) { - $context && $message.= ' ('.$context.')'; - trigger_error($message.' in '.$this->file.' on line '.$this->token->line); - } - - /** - * Forward this parser to the next token - * - * @return void - */ - public function forward() { - static $line= 1; - - if ($this->queue) { - $this->token= array_shift($this->queue); - return; - } - - while ($this->tokens->valid()) { - $type= $this->tokens->key(); - list($value, $line)= $this->tokens->current(); - $this->tokens->next(); - if ('name' === $type) { - $t= new Token($this->language->symbols[$value] ?? $this->language->symbol('(name)')); - $t->kind= $type; - } else if ('operator' === $type) { - $t= new Token($this->language->symbol($value)); - $t->kind= $type; - } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $t= new Token($this->language->symbol('(literal)')); - $t->kind= 'literal'; - } else if ('variable' === $type) { - $t= new Token($this->language->symbol('(variable)')); - $t->kind= 'variable'; - } else if ('comment' === $type) { - $this->comment= $value; - continue; - } else { - throw new Error('Unexpected token '.$value, $this->file, $line); - } - - $t->value= $value; - $t->line= $line; - $this->token= $t; - return; - } - - $t= new Token($this->language->symbol('(end)')); - $t->line= $line; - $this->token= $t; - } - - /** - * Forward expecting a given token, raise an error if another is encountered - * - * @param string $id - * @param string $context - * @return void - */ - public function expecting($id, $context) { - if ($id === $this->token->symbol->id) { - $this->forward(); - return; - } - - $message= sprintf( - 'Expected "%s", have "%s" in %s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context - ); - $e= new Error($message, $this->file, $this->token->line); - - // Ensure we stop if we encounter the end - if (null === $this->token->value) { - throw $e; - } else { - $this->errors[]= $e; - } - } - - /** - * Parses given file, returning AST nodes. - * - * @return iterable - * @throws lang.ast.Errors - */ - public function execute() { - $this->forward(); - try { - while (null !== $this->token->value) { - if (null === ($statement= $this->language->statement($this))) break; - yield $statement; - } - } catch (Error $e) { - $this->errors[]= $e; - } - - if ($this->errors) { - throw new Errors($this->errors, $this->file); - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Scope.class.php b/src/main/php/lang/ast/Scope.class.php deleted file mode 100755 index ecf95a10..00000000 --- a/src/main/php/lang/ast/Scope.class.php +++ /dev/null @@ -1,79 +0,0 @@ - true, - 'int' => true, - 'float' => true, - 'double' => true, - 'bool' => true, - 'array' => true, - 'void' => true, - 'callable' => true, - 'iterable' => true, - 'object' => true, - 'self' => true, - 'static' => true, - 'parent' => true, - 'mixed' => true - ]; - - public $parent; - public $package= null; - public $imports= []; - public $annotations= []; - - public function __construct(self $parent= null) { - $this->parent= $parent; - } - - /** - * Sets package - * - * @param string $name - * @return void - */ - public function package($name) { - $this->package= '\\'.$name; - } - - /** - * Adds an import - * - * @param string $name - * @param string $alias - * @return void - */ - public function import($name, $alias= null) { - $this->imports[$alias ?: substr($name, strrpos($name, '\\') + 1)]= '\\'.$name; - } - - /** - * Resolves a type to a fully qualified name - * - * @param string $name - * @return string - */ - public function resolve($name) { - if (null === $name) { - return ''; - } else if (isset(self::$reserved[$name])) { - return $name; - } else if ('\\' === $name[0]) { - return $name; - } else if (isset($this->imports[$name])) { - return $this->imports[$name]; - } else if ($this->package) { - return $this->package.'\\'.$name; - } else if ($this->parent) { - return $this->parent->resolve($name); - } else { - return '\\'.$name; - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Symbol.class.php b/src/main/php/lang/ast/Symbol.class.php deleted file mode 100755 index 6d4c79c5..00000000 --- a/src/main/php/lang/ast/Symbol.class.php +++ /dev/null @@ -1,27 +0,0 @@ -id; - } - - /** @return string */ - public function toString() { - return nameof($this).'("'.$this->id.'", lbp= '.$this->lbp.')'; - } - - /** - * Compare - * - * @param var $value - * @return int - */ - public function compareTo($value) { - return $value instanceof self ? strcmp($this->id, $value->id) : 1; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Token.class.php b/src/main/php/lang/ast/Token.class.php deleted file mode 100755 index 223b4a25..00000000 --- a/src/main/php/lang/ast/Token.class.php +++ /dev/null @@ -1,43 +0,0 @@ -symbol= $symbol; - $this->kind= $kind; - $this->value= $value; - } - - /** @return string */ - public function hashCode() { - return $this->symbol->hashCode().$this->kind.Objects::hashOf($this->value); - } - - /** @return string */ - public function toString() { - return nameof($this).'(kind= '.$this->kind.', value= '.Objects::stringOf($this->value).')'; - } - - /** - * Compares this node to another given value - * - * @param var $value - * @return int - */ - public function compareTo($value) { - return $value === $this ? 0 : 1; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php deleted file mode 100755 index 03f34d71..00000000 --- a/src/main/php/lang/ast/Tokens.class.php +++ /dev/null @@ -1,136 +0,0 @@ -(){}[]#+-*/'\$\"\r\n\t"; - - private static $operators= [ - '<' => ['<=', '<<', '<>', '', '<<='], - '>' => ['>=', '>>', '>>='], - '=' => ['=>', '==', '==='], - '!' => ['!=', '!=='], - '&' => ['&&', '&='], - '|' => ['||', '|='], - '^' => ['^='], - '+' => ['+=', '++'], - '-' => ['-=', '--', '->'], - '*' => ['*=', '**', '**='], - '/' => ['/='], - '~' => ['~='], - '%' => ['%='], - '?' => ['?:', '??', '?->', '??='], - '.' => ['.=', '...'], - ':' => ['::'], - "\303" => ["\303\227"] - ]; - - private $source; - - /** - * Create new iterable tokens from a string or a stream tokenizer - * - * @param text.Tokenizer $source - */ - public function __construct(Tokenizer $source) { - $this->source= $source; - $this->source->delimiters= self::DELIMITERS; - $this->source->returnDelims= true; - } - - /** @return php.Iterator */ - public function getIterator() { - $line= 1; - while (null !== ($token= $this->source->nextToken())) { - if ('$' === $token) { - yield 'variable' => [$this->source->nextToken(), $line]; - } else if ('"' === $token || "'" === $token) { - $string= $token; - $end= '\\'.$token; - do { - $t= $this->source->nextToken($end); - if (null === $t) { - throw new FormatException('Unclosed string literal starting at line '.$line); - } else if ('\\' === $t) { - $string.= $t.$this->source->nextToken($end); - } else { - $string.= $t; - } - } while ($token !== $t); - - yield 'string' => [$string, $line]; - $line+= substr_count($string, "\n"); - } else if ("\n" === $token) { - $line++; - } else if ("\r" === $token || "\t" === $token || ' ' === $token) { - // Skip - } else if (0 === strcspn($token, '0123456789')) { - if ('.' === ($next= $this->source->nextToken())) { - yield 'decimal' => [str_replace('_', '', $token.$next.$this->source->nextToken()), $line]; - } else { - $this->source->pushBack($next); - yield 'integer' => [str_replace('_', '', $token), $line]; - } - } else if (0 === strcspn($token, self::DELIMITERS)) { - if ('.' === $token) { - $next= $this->source->nextToken(); - if (0 === strcspn($next, '0123456789')) { - yield 'decimal' => [".$next", $line]; - continue; - } - $this->source->pushBack($next); - } else if ('/' === $token) { - $next= $this->source->nextToken(); - if ('/' === $next) { - $this->source->nextToken("\r\n"); - continue; - } else if ('*' === $next) { - $comment= ''; - do { - $t= $this->source->nextToken('/'); - $comment.= $t; - } while ('*' !== $t[strlen($t)- 1] && $this->source->hasMoreTokens()); - $comment.= $this->source->nextToken('/'); - yield 'comment' => [trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 1, -2))), $line]; - $line+= substr_count($comment, "\n"); - continue; - } - $this->source->pushBack($next); - } else if ('#' === $token) { - $comment= $this->source->nextToken("\r\n").$this->source->nextToken("\r\n"); - $next= '#'; - do { - $s= strspn($next, ' '); - if ('#' !== $next[$s]) break; - $line++; - $comment.= substr($next, $s + 1); - $next= $this->source->nextToken("\r\n").$this->source->nextToken("\r\n"); - } while ($this->source->hasMoreTokens()); - if (0 === strncmp($comment, '[@', 2)) { - $this->source->pushBack(substr($comment, 1).$next); - yield 'operator' => ['#[', $line]; - } else { - $this->source->pushBack($next); - } - continue; - } - - if (isset(self::$operators[$token])) { - $combined= $token; - foreach (self::$operators[$token] as $operator) { - while (strlen($combined) < strlen($operator) && $this->source->hasMoreTokens()) { - $combined.= $this->source->nextToken(); - } - $combined === $operator && $token= $combined; - } - - $this->source->pushBack(substr($combined, strlen($token))); - } - yield 'operator' => [$token, $line]; - } else { - yield 'name' => [$token, $line]; - } - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/Extension.class.php b/src/main/php/lang/ast/syntax/Extension.class.php deleted file mode 100755 index 403c0688..00000000 --- a/src/main/php/lang/ast/syntax/Extension.class.php +++ /dev/null @@ -1,13 +0,0 @@ -symbol(':'); - $this->symbol(';'); - $this->symbol(','); - $this->symbol(')'); - $this->symbol(']'); - $this->symbol('}'); - $this->symbol('as'); - $this->symbol('const'); - $this->symbol('(end)'); - $this->symbol('(name)'); - $this->symbol('(literal)'); - $this->symbol('(variable)'); - - $this->constant('true', 'true'); - $this->constant('false', 'false'); - $this->constant('null', 'null'); - - $this->infixr('??', 30); - $this->infixr('?:', 30); - $this->infixr('&&', 30); - $this->infixr('||', 30); - - $this->infixr('==', 40); - $this->infixr('===', 40); - $this->infixr('!=', 40); - $this->infixr('!==', 40); - $this->infixr('<', 40); - $this->infixr('<=', 40); - $this->infixr('>', 40); - $this->infixr('>=', 40); - $this->infixr('<=>', 40); - - $this->infix('+', 50); - $this->infix('-', 50); - $this->infix('&', 50); - $this->infix('|', 50); - $this->infix('^', 50); - $this->suffix('++', 50); - $this->suffix('--', 50); - - $this->infix('*', 60); - $this->infix('/', 60); - $this->infix('%', 60); - $this->infix('.', 60); - $this->infix('**', 60); - - $this->infixr('<<', 70); - $this->infixr('>>', 70); - - $this->infix('instanceof', 110, function($parse, $token, $left) { - $type= $this->expression($parse, 110); - if ($type instanceof Literal) { - return new InstanceOfExpression($left, $parse->scope->resolve($type->expression), $token->line); - } else { - return new InstanceOfExpression($left, $type, $token->line); - } - }); - - $this->infix('->', 100, function($parse, $token, $left) { - if ('{' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - $parse->expecting('}', 'dynamic member'); - } else { - $expr= new Literal($parse->token->value, $token->line); - $parse->forward(); - } - - return new InstanceExpression($left, $expr, $token->line); - }); - - $this->infix('::', 120, function($parse, $token, $left) { - $scope= $left instanceof Literal ? $parse->scope->resolve($left->expression) : $left; - - if ('variable' === $parse->token->kind) { - $expr= new Variable($parse->token->value, $parse->token->line); - } else if ('name' === $parse->token->kind) { - $expr= new Literal($parse->token->value, $parse->token->line); - } else { - $parse->expecting('name or variable', '::'); - $expr= null; - } - - $parse->forward(); - if ('(' === $parse->token->value) { - $parse->expecting('(', 'invoke expression'); - $arguments= $this->expressions($parse); - $parse->expecting(')', 'invoke expression'); - $expr= new InvokeExpression($expr, $arguments, $token->line); - } - - return new ScopeExpression($scope, $expr, $token->line); - }); - - $this->infix('(', 100, function($parse, $token, $left) { - $arguments= $this->expressions($parse); - $parse->expecting(')', 'invoke expression'); - return new InvokeExpression($left, $arguments, $token->line); - }); - - $this->infix('[', 100, function($parse, $token, $left) { - if (']' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $this->expression($parse, 0); - $parse->expecting(']', 'offset access'); - } - - return new OffsetExpression($left, $expr, $token->line); - }); - - $this->infix('{', 100, function($parse, $token, $left) { - $parse->warn('Deprecated curly braces use as offset'); - $expr= $this->expression($parse, 0); - $parse->expecting('}', 'offset'); - return new OffsetExpression($left, $expr, $token->line); - }); - - $this->infix('?', 80, function($parse, $token, $left) { - $when= $this->expression($parse, 0); - $parse->expecting(':', 'ternary'); - $else= $this->expression($parse, 0); - return new TernaryExpression($left, $when, $else, $token->line); - }); - - $this->prefix('@', 90); - $this->prefix('&', 90); - $this->prefix('!', 90); - $this->prefix('~', 90); - $this->prefix('+', 90); - $this->prefix('-', 90); - $this->prefix('++', 90); - $this->prefix('--', 90); - $this->prefix('clone', 90); - - $this->assignment('='); - $this->assignment('&='); - $this->assignment('|='); - $this->assignment('^='); - $this->assignment('+='); - $this->assignment('-='); - $this->assignment('*='); - $this->assignment('/='); - $this->assignment('.='); - $this->assignment('**='); - $this->assignment('>>='); - $this->assignment('<<='); - $this->assignment('??='); - - // This is ambiguous: - // - // - An arrow function `($a) ==> $a + 1` - // - An expression surrounded by parentheses `($a ?? $b)->invoke()`; - // - A cast `(int)$a` or `(int)($a / 2)`. - // - // Resolve by looking ahead after the closing ")" - $this->prefix('(', 0, function($parse, $token) { - static $types= [ - '<' => true, - '>' => true, - ',' => true, - '?' => true, - ':' => true - ]; - - $skipped= [$token, $parse->token]; - $cast= true; - $level= 1; - while ($level > 0 && null !== $parse->token->value) { - if ('(' === $parse->token->value) { - $level++; - } else if (')' === $parse->token->value) { - $level--; - } else if ('name' !== $parse->token->kind && !isset($types[$parse->token->value])) { - $cast= false; - } - $parse->forward(); - $skipped[]= $parse->token; - } - $parse->queue= array_merge($skipped, $parse->queue); - - if ($cast && ('operator' !== $parse->token->kind || '(' === $parse->token->value || '[' === $parse->token->value)) { - $parse->forward(); - $parse->expecting('(', 'cast'); - $type= $this->type0($parse, false); - $parse->expecting(')', 'cast'); - return new CastExpression($type, $this->expression($parse, 0), $token->line); - } else { - $parse->forward(); - $parse->expecting('(', 'braced'); - $expr= $this->expression($parse, 0); - $parse->expecting(')', 'braced'); - return new Braced($expr, $token->line); - } - }); - - $this->prefix('[', 0, function($parse, $token) { - $values= []; - while (']' !== $parse->token->value) { - $expr= $this->expression($parse, 0); - - if ('=>' === $parse->token->value) { - $parse->forward(); - $values[]= [$expr, $this->expression($parse, 0)]; - } else { - $values[]= [null, $expr]; - } - - if (']' === $parse->token->value) { - break; - } else { - $parse->expecting(',', 'array literal'); - } - } - - $parse->expecting(']', 'array literal'); - return new ArrayLiteral($values, $token->line); - }); - - $this->prefix('new', 0, function($parse, $token) { - if ('(' === $parse->token->value) { - $parse->expecting('(', 'new type'); - $type= $this->expression($parse, 0); - $parse->expecting(')', 'new type'); - } else if ('variable' === $parse->token->kind) { - $type= '$'.$parse->token->value; - $parse->forward(); - } else if ('class' === $parse->token->value) { - $type= null; - $parse->forward(); - } else { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - } - - $parse->expecting('(', 'new arguments'); - $arguments= $this->expressions($parse); - $parse->expecting(')', 'new arguments'); - - if (null === $type) { - return new NewClassExpression($this->clazz($parse, null), $arguments, $token->line); - } else { - return new NewExpression($type, $arguments, $token->line); - } - }); - - $this->prefix('yield', 0, function($parse, $token) { - if (';' === $parse->token->value) { - return new YieldExpression(null, null, $token->line); - } else if ('from' === $parse->token->value) { - $parse->forward(); - return new YieldFromExpression($this->expression($parse, 0), $token->line); - } else { - $expr= $this->expression($parse, 0); - if ('=>' === $parse->token->value) { - $parse->forward(); - return new YieldExpression($expr, $this->expression($parse, 0), $token->line); - } else { - return new YieldExpression(null, $expr, $token->line); - } - } - }); - - $this->prefix('...', 0, function($parse, $token) { - return new UnpackExpression($this->expression($parse, 0), $token->line); - }); - - $this->prefix('fn', 0, function($parse, $token) { - $signature= $this->signature($parse); - $parse->expecting('=>', 'fn'); - - if ('{' === $parse->token->value) { - $parse->expecting('{', 'fn'); - $statements= $this->statements($parse); - $parse->expecting('}', 'fn'); - } else { - $statements= $this->expression($parse, 0); - } - - return new LambdaExpression($signature, $statements, $token->line); - }); - - $this->prefix('function', 0, function($parse, $token) { - - // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; - // the latter explicitely becomes a statement by pushing a semicolon. - if ('(' === $parse->token->value) { - $signature= $this->signature($parse); - - if ('use' === $parse->token->value) { - $parse->forward(); - $parse->forward(); - $use= []; - while (')' !== $parse->token->value) { - if ('&' === $parse->token->value) { - $parse->forward(); - $use[]= '&$'.$parse->token->value; - } else { - $use[]= '$'.$parse->token->value; - } - $parse->forward(); - if (')' === $parse->token->value) break; - $parse->expecting(',', 'use list'); - } - $parse->expecting(')', 'closure'); - } else { - $use= null; - } - - $parse->expecting('{', 'function'); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); - - return new ClosureExpression($signature, $use, $statements, $token->line); - } else { - $name= $parse->token->value; - $parse->forward(); - $signature= $this->signature($parse); - - $parse->expecting('{', 'function'); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); - - $parse->queue= [$parse->token]; - $parse->token= new Token($this->symbol(';')); - return new FunctionDeclaration($name, $signature, $statements, $token->line); - } - }); - - $this->prefix('static', 0, function($parse, $token) { - if ('variable' === $parse->token->kind) { - $init= []; - while (';' !== $parse->token->value) { - $variable= $parse->token->value; - $parse->forward(); - - if ('=' === $parse->token->value) { - $parse->forward(); - $init[$variable]= $this->expression($parse, 0); - } else { - $init[$variable]= null; - } - - if (',' === $parse->token->value) { - $parse->forward(); - } - } - return new StaticLocals($init, $token->line); - } - return new Literal($token->value, $token->line); - }); - - $this->prefix('goto', 0, function($parse, $token) { - $label= $parse->token->value; - $parse->forward(); - return new GotoStatement($label, $token->line); - }); - - $this->prefix('(variable)', 0, function($parse, $token) { - return new Variable($token->value, $token->line); - }); - - $this->prefix('(literal)', 0, function($parse, $token) { - return new Literal($token->value, $token->line); - }); - - $this->prefix('(name)', 0, function($parse, $token) { - return new Literal($token->value, $token->line); - }); - - $this->stmt('(name)', function($parse, $token) { - - // Solve ambiguity between goto-labels and other statements - if (':' === $parse->token->value) { - $node= new Label($token->value, $token->line); - } else { - $parse->queue[]= $parse->token; - $parse->token= $token; - $node= $this->expression($parse, 0); - } - $parse->forward(); - return $node; - }); - - $this->stmt('token->value; - $parse->forward(); - return new Start($syntax, $token->line); - }); - - $this->stmt('{', function($parse, $token) { - $statements= $this->statements($parse); - $parse->expecting('}', 'block'); - return new Block($statements, $token->line); - }); - - $this->prefix('echo', 0, function($parse, $token) { - return new EchoStatement($this->expressions($parse, ';'), $token->line); - }); - - $this->stmt('namespace', function($parse, $token) { - $name= $parse->token->value; - $parse->forward(); - $parse->expecting(';', 'namespace'); - $parse->scope->package($name); - return new NamespaceDeclaration($name, $token->line); - }); - - $this->stmt('use', function($parse, $token) { - if ('function' === $parse->token->value) { - $type= 'function'; - $parse->forward(); - } else if ('const' === $parse->token->value) { - $type= 'const'; - $parse->forward(); - } else { - $type= null; // class, interface or trait - } - - $import= $parse->token->value; - $parse->forward(); - - if ('{' === $parse->token->value) { - $names= []; - $parse->forward(); - while ('}' !== $parse->token->value) { - $class= $import.$parse->token->value; - - $parse->forward(); - if ('as' === $parse->token->value) { - $parse->forward(); - $names[$class]= $parse->token->value; - $parse->scope->import($parse->token->value); - $parse->forward(); - } else { - $names[$class]= null; - $parse->scope->import($class); - } - - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('}' === $parse->token->value) { - break; - } else { - $this->expecting(', or }', 'use'); - break; - } - } - $parse->forward(); - } else if ('as' === $parse->token->value) { - $parse->forward(); - $names= [$import => $parse->token->value]; - $parse->scope->import($import, $parse->token->value); - $parse->forward(); - } else { - $names= [$import => null]; - $parse->scope->import($import); - } - - $parse->expecting(';', 'use'); - return new UseStatement($type, $names, $token->line); - }); - - $this->stmt('if', function($parse, $token) { - $parse->expecting('(', 'if'); - $condition= $this->expression($parse, 0); - $parse->expecting(')', 'if'); - $when= $this->block($parse); - if ('else' === $parse->token->value) { - $parse->forward(); - $otherwise= $this->block($parse); - } else { - $otherwise= null; - } - - return new IfStatement($condition, $when, $otherwise, $token->line); - }); - - $this->stmt('switch', function($parse, $token) { - $parse->expecting('(', 'switch'); - $condition= $this->expression($parse, 0); - $parse->expecting(')', 'switch'); - - $cases= []; - $parse->expecting('{', 'switch'); - while ('}' !== $parse->token->value) { - if ('default' === $parse->token->value) { - $parse->forward(); - $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel(null, [], $parse->token->line); - } else if ('case' === $parse->token->value) { - $parse->forward(); - - if ('name' === $parse->token->kind) { - $expr= new Literal($parse->token->value, $parse->token->line); - $parse->forward(); - } else { - $expr= $this->expression($parse, 0); - } - $parse->expecting(':', 'switch'); - $cases[]= new CaseLabel($expr, [], $parse->token->line); - } else { - $cases[sizeof($cases) - 1]->body[]= $this->statement($parse); - } - } - $parse->forward(); - - return new SwitchStatement($condition, $cases, $token->line); - }); - - $this->stmt('break', function($parse, $token) { - if (';' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $this->expression($parse, 0); - $parse->expecting(';', 'break'); - } - - return new BreakStatement($expr, $token->line); - }); - - $this->stmt('continue', function($parse, $token) { - if (';' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $this->expression($parse, 0); - $parse->expecting(';', 'continue'); - } - - return new ContinueStatement($expr, $token->line); - }); - - $this->stmt('do', function($parse, $token) { - $block= $this->block($parse); - $parse->expecting('while', 'do'); - $parse->expecting('(', 'do'); - $expression= $this->expression($parse, 0); - $parse->expecting(')', 'do'); - $parse->expecting(';', 'do'); - return new DoLoop($expression, $block, $token->line); - }); - - $this->stmt('while', function($parse, $token) { - $parse->expecting('(', 'while'); - $expression= $this->expression($parse, 0); - $parse->expecting(')', 'while'); - $block= $this->block($parse); - return new WhileLoop($expression, $block, $token->line); - }); - - $this->stmt('for', function($parse, $token) { - $parse->expecting('(', 'for'); - $init= $this->expressions($parse, ';'); - $parse->expecting(';', 'for'); - $cond= $this->expressions($parse, ';'); - $parse->expecting(';', 'for'); - $loop= $this->expressions($parse, ')'); - $parse->expecting(')', 'for'); - $block= $this->block($parse); - return new ForLoop($init, $cond, $loop, $block, $token->line); - }); - - $this->stmt('foreach', function($parse, $token) { - $parse->expecting('(', 'foreach'); - $expression= $this->expression($parse, 0); - $parse->expecting('as', 'foreach'); - $expr= $this->expression($parse, 0); - - if ('=>' === $parse->token->value) { - $parse->forward(); - $key= $expr; - $value= $this->expression($parse, 0); - } else { - $key= null; - $value= $expr; - } - - $parse->expecting(')', 'foreach'); - $block= $this->block($parse); - return new ForeachLoop($expression, $key, $value, $block, $token->line); - }); - - $this->stmt('throw', function($parse, $token) { - $expr= $this->expression($parse, 0); - $parse->expecting(';', 'throw'); - return new ThrowStatement($expr, $token->line); - }); - - $this->stmt('try', function($parse, $token) { - $parse->expecting('{', 'try'); - $statements= $this->statements($parse); - $parse->expecting('}', 'try'); - - $catches= []; - while ('catch' === $parse->token->value) { - $parse->forward(); - $parse->expecting('(', 'try'); - - $types= []; - while ('name' === $parse->token->kind) { - $types[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if ('|' !== $parse->token->value) break; - $parse->forward(); - } - - $variable= $parse->token; - $parse->forward(); - $parse->expecting(')', 'catch'); - - $parse->expecting('{', 'catch'); - $catches[]= new CatchStatement($types, $variable->value, $this->statements($parse), $parse->token->line); - $parse->expecting('}', 'catch'); - } - - if ('finally' === $parse->token->value) { - $parse->forward(); - $parse->expecting('{', 'finally'); - $finally= $this->statements($parse); - $parse->expecting('}', 'finally'); - } else { - $finally= null; - } - - return new TryStatement($statements, $catches, $finally, $token->line); - }); - - $this->stmt('return', function($parse, $token) { - if (';' === $parse->token->value) { - $expr= null; - $parse->forward(); - } else { - $expr= $this->expression($parse, 0); - $parse->expecting(';', 'return'); - } - - return new ReturnStatement($expr, $token->line); - }); - - $this->stmt('abstract', function($parse, $token) { - $type= $this->statement($parse); - $type->modifiers[]= 'abstract'; - return $type; - }); - - $this->stmt('final', function($parse, $token) { - $type= $this->statement($parse); - $type->modifiers[]= 'final'; - return $type; - }); - - $this->stmt('<<', function($parse, $token) { - $parse->scope->annotations= $this->annotations($parse, 'annotations'); - - return new Annotations($parse->scope->annotations, $token->line); - }); - - $this->stmt('#[', function($parse, $token) { - $parse->scope->annotations= $this->meta($parse, 'annotations')[DETAIL_ANNOTATIONS]; - - return new Annotations($parse->scope->annotations, $token->line); - }); - - $this->stmt('class', function($parse, $token) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - - return $this->clazz($parse, $type); - }); - - $this->stmt('interface', function($parse, $token) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - $comment= $parse->comment; - $parse->comment= null; - - $parents= []; - if ('extends' === $parse->token->value) { - $parse->forward(); - do { - $parents[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('{' === $parse->token->value) { - break; - } else { - $parse->expecting(', or {', 'interface parents'); - } - } while (null !== $parse->token->value); - } - - $parse->expecting('{', 'interface'); - $body= $this->typeBody($parse); - $parse->expecting('}', 'interface'); - - $decl= new InterfaceDeclaration([], $type, $parents, $body, $parse->scope->annotations, $comment, $token->line); - $parse->scope->annotations= []; - return $decl; - }); - - $this->stmt('trait', function($parse, $token) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - $comment= $parse->comment; - $parse->comment= null; - - $parse->expecting('{', 'trait'); - $body= $this->typeBody($parse); - $parse->expecting('}', 'trait'); - - $decl= new TraitDeclaration([], $type, $body, $parse->scope->annotations, $comment, $token->line); - $parse->scope->annotations= []; - return $decl; - }); - - $this->body('use', function($parse, &$body, $annotations, $modifiers) { - $line= $parse->token->line; - - $parse->forward(); - $types= []; - do { - $types[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else { - break; - } - } while ($parse->token->value); - - $aliases= []; - if ('{' === $parse->token->value) { - $parse->forward(); - while ('}' !== $parse->token->value) { - $method= $parse->token->value; - $parse->forward(); - if ('::' === $parse->token->value) { - $parse->forward(); - $method= $parse->scope->resolve($method).'::'.$parse->token->value; - $parse->forward(); - } - $parse->expecting('as', 'use'); - $alias= $parse->token->value; - $parse->forward(); - $parse->expecting(';', 'use'); - $aliases[$method]= $alias; - } - $parse->expecting('}', 'use'); - } else { - $parse->expecting(';', 'use'); - } - - $body[]= new UseExpression($types, $aliases, $line); - }); - - $this->body('const', function($parse, &$body, $annotations, $modifiers) { - $parse->forward(); - - $type= null; - while (';' !== $parse->token->value) { - $line= $parse->token->line; - $first= $parse->token; - $parse->forward(); - - // Untyped `const T = 5` vs. typed `const int T = 5` - if ('=' === $parse->token->value) { - $name= $first->value; - } else { - $parse->queue[]= $first; - $parse->queue[]= $parse->token; - $parse->token= $first; - - $type= $this->type($parse, false); - $parse->forward(); - $name= $parse->token->value; - $parse->forward(); - } - - if (isset($body[$name])) { - $parse->raise('Cannot redeclare constant '.$name); - } - - $parse->expecting('=', 'const'); - $body[$name]= new Constant($modifiers, $name, $type, $this->expression($parse, 0), $line); - if (',' === $parse->token->value) { - $parse->forward(); - } - } - $parse->expecting(';', 'constant declaration'); - }); - - $this->body('@variable', function($parse, &$body, $meta, $modifiers) { - $this->properties($parse, $body, $meta, $modifiers, null); - }); - - $this->body('function', function($parse, &$body, $meta, $modifiers) { - $line= $parse->token->line; - $comment= $parse->comment; - $parse->comment= null; - - $parse->forward(); - $name= $parse->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $parse->raise('Cannot redeclare method '.$lookup); - } - - $parse->forward(); - $signature= $this->signature($parse, isset($meta[DETAIL_TARGET_ANNO]) ? $meta[DETAIL_TARGET_ANNO] : []); - - if ('{' === $parse->token->value) { // Regular body - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'method declaration'); - } else if (';' === $parse->token->value) { // Abstract or interface method - $statements= null; - $parse->expecting(';', 'method declaration'); - } else { - $parse->expecting('{ or ;', 'method declaration'); - } - - $body[$lookup]= new Method( - $modifiers, - $name, - $signature, - $statements, - isset($meta[DETAIL_ANNOTATIONS]) ? $meta[DETAIL_ANNOTATIONS] : [], - $comment, - $line - ); - }); - } - - private function type($parse, $optional= true) { - $t= []; - do { - $t[]= $this->type0($parse, $optional); - if ('|' === $parse->token->value) { - $parse->forward(); - continue; - } - return 1 === sizeof($t) ? $t[0] : new UnionType($t); - } while (true); - } - - private function type0($parse, $optional) { - if ('?' === $parse->token->value) { - $parse->forward(); - $type= '?'.$parse->scope->resolve($parse->token->value); - $parse->forward(); - } else if ('(' === $parse->token->value) { - $parse->forward(); - $type= $this->type($parse, false); - $parse->forward(); - return $type; - } else if ('name' === $parse->token->kind && 'function' === $parse->token->value) { - $parse->forward(); - $parse->expecting('(', 'type'); - $signature= []; - if (')' !== $parse->token->value) do { - $signature[]= $this->type($parse, false); - if (',' === $parse->token->value) { - $parse->forward(); - } else if (')' === $parse->token->value) { - break; - } else { - $parse->expecting(', or )', 'function type'); - } - } while (null !== $parse->token->value); - $parse->expecting(')', 'type'); - $parse->expecting(':', 'type'); - return new FunctionType($signature, $this->type($parse, false)); - } else if ('name' === $parse->token->kind) { - $type= $parse->scope->resolve($parse->token->value); - $parse->forward(); - } else if ($optional) { - return null; - } else { - $parse->expecting('type name', 'type'); - return null; - } - - if ('<' === $parse->token->value) { - $parse->forward(); - $components= []; - do { - $components[]= $this->type($parse, false); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('>' === $parse->token->symbol->id) { - break; - } else if ('>>' === $parse->token->value) { - $parse->queue[]= $parse->token= new Token(self::symbol('>')); - break; - } - } while (true); - $parse->expecting('>', 'type'); - - if ('array' === $type) { - return 1 === sizeof($components) ? new ArrayType($components[0]) : new MapType($components[0], $components[1]); - } else { - return new GenericType($type, $components); - } - } else { - return new Type($type); - } - } - - private function properties($parse, &$body, $meta, $modifiers, $type) { - $comment= $parse->comment; - $parse->comment= null; - $annotations= isset($meta[DETAIL_ANNOTATIONS]) ? $meta[DETAIL_ANNOTATIONS] : []; - - while (';' !== $parse->token->value) { - $line= $parse->token->line; - - // Untyped `$a` vs. typed `int $a` - if ('variable' === $parse->token->kind) { - $name= $parse->token->value; - } else { - $type= $this->type($parse, false); - $name= $parse->token->value; - } - - $lookup= '$'.$name; - if (isset($body[$lookup])) { - $parse->raise('Cannot redeclare property '.$lookup); - } - - $parse->forward(); - if ('=' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - } else { - $expr= null; - } - $body[$lookup]= new Property($modifiers, $name, $type, $expr, $annotations, $comment, $line); - - if (',' === $parse->token->value) { - $parse->forward(); - } - } - $parse->expecting(';', 'field declaration'); - } - - /** Parses Hacklang-style annotations (<>) */ - private function annotations($parse, $context) { - $annotations= []; - do { - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->expecting('(', $context); - $annotations[$name]= $this->expression($parse, 0); - $parse->expecting(')', $context); - } else { - $annotations[$name]= null; - } - - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else if ('>>' === $parse->token->value) { - break; - } else { - $parse->expecting(', or >>', $context); - } - } while (null !== $parse->token->value); - - $parse->expecting('>>', $context); - return $annotations; - } - - /** Parses XP-style annotations (#[@test]) */ - private function meta($parse, $context) { - $meta= [DETAIL_ANNOTATIONS => [], DETAIL_TARGET_ANNO => []]; - do { - $parse->expecting('@', $context); - - if ('variable' === $parse->token->kind) { - $param= $parse->token->value; - $parse->forward(); - $parse->expecting(':', $context); - $a= &$meta[DETAIL_TARGET_ANNO][$param]; - } else { - $a= &$meta[DETAIL_ANNOTATIONS]; - } - - $name= $parse->token->value; - $parse->forward(); - - if ('(' === $parse->token->value) { - $parse->expecting('(', $context); - - if ('name' === $parse->token->kind) { - $token= $parse->token; - $parse->forward(); - $pairs= '=' === $parse->token->value; - - $parse->queue[]= $parse->token; - $parse->token= $token; - - if ($pairs) { - $line= $parse->token->line; - $values= []; - do { - $key= $parse->token->value; - $parse->warn('Use of deprecated annotation key/value pair "'.$key.'"', $context); - $parse->forward(); - $parse->expecting('=', $context); - - $values[]= [new Literal("'".$key."'"), $this->expression($parse, 0)]; - - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } - break; - } while (null !== $parse->token->value); - $a[$name]= new ArrayLiteral($values, $line); - } else { - $a[$name]= $this->expression($parse, 0); - } - } else { - $a[$name]= $this->expression($parse, 0); - } - $parse->expecting(')', $context); - } else { - $a[$name]= null; - } - - if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else if (']' === $parse->token->value) { - break; - } else { - $parse->expecting(', or ]', $context); - } - } while (null !== $parse->token->value); - - $parse->expecting(']', $context); - return $meta; - } - - private function parameters($parse, $target) { - static $promotion= ['private' => true, 'protected' => true, 'public' => true]; - - $parameters= []; - while (')' !== $parse->token->value) { - if ('<<' === $parse->token->value) { - $parse->forward(); - $annotations= $this->annotations($parse, 'parameter annotation'); - } else { - $annotations= []; - } - - if ('name' === $parse->token->kind && isset($promotion[$parse->token->value])) { - $promote= $parse->token->value; - $parse->forward(); - } else { - $promote= null; - } - - $type= $this->type($parse); - - if ('...' !== $parse->token->value) { - $variadic= false; - } else if ($promote) { - $parse->raise('Variadic parameters cannot be promoted', 'parameters'); - $variadic= true; - $parse->forward(); - } else { - $variadic= true; - $parse->forward(); - } - - if ('&' === $parse->token->value) { - $byref= true; - $parse->forward(); - } else { - $byref= false; - } - - $name= $parse->token->value; - if (isset($target[$name])) $annotations= array_merge($annotations, $target[$name]); - $parse->forward(); - - $default= null; - if ('=' === $parse->token->value) { - $parse->forward(); - $default= $this->expression($parse, 0); - } - $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); - - if (')' === $parse->token->value) { - break; - } else if (',' === $parse->token->value) { - $parse->forward(); - continue; - } else { - $parse->expecting(',', 'parameter list'); - break; - } - } - return $parameters; - } - - public function body($id, $func) { - $this->body[$id]= $func->bindTo($this, static::class); - } - - public function typeBody($parse) { - static $modifier= [ - 'private' => true, - 'protected' => true, - 'public' => true, - 'static' => true, - 'final' => true, - 'abstract' => true - ]; - - $body= []; - $modifiers= []; - $meta= []; - while ('}' !== $parse->token->value) { - if (isset($modifier[$parse->token->value])) { - $modifiers[]= $parse->token->value; - $parse->forward(); - } else if ($f= $this->body[$parse->token->value] ?? $this->body['@'.$parse->token->kind] ?? null) { - $f($parse, $body, $meta, $modifiers); - $modifiers= []; - $meta= []; - } else if ('<<' === $parse->token->value) { - $parse->forward(); - $meta= [DETAIL_ANNOTATIONS => $this->annotations($parse, 'member annotations')]; - } else if ('#[' === $parse->token->value) { - $parse->forward(); - $meta= $this->meta($parse, 'member annotations'); - } else if ($type= $this->type($parse)) { - $this->properties($parse, $body, $meta, $modifiers, $type); - $modifiers= []; - $meta= []; - } else { - $parse->raise(sprintf( - 'Expected a type, modifier, property, annotation, method or "}", have "%s"', - $parse->token->symbol->id - )); - $parse->forward(); - if (null === $parse->token->value) break; - } - } - return $body; - } - - public function signature($parse, $annotations= []) { - $parse->expecting('(', 'signature'); - $parameters= $this->parameters($parse, $annotations); - $parse->expecting(')', 'signature'); - - if (':' === $parse->token->value) { - $parse->forward(); - $return= $this->type($parse); - } else { - $return= null; - } - - return new Signature($parameters, $return); - } - - public function block($parse) { - if ('{' === $parse->token->value) { - $parse->forward(); - $block= $this->statements($parse); - $parse->expecting('}', 'block'); - return $block; - } else { - return [$this->statement($parse)]; - } - } - - public function clazz($parse, $name, $modifiers= []) { - $comment= $parse->comment; - $parse->comment= null; - $line= $parse->token->line; - - $parent= null; - if ('extends' === $parse->token->value) { - $parse->forward(); - $parent= $parse->scope->resolve($parse->token->value); - $parse->forward(); - } - - $implements= []; - if ('implements' === $parse->token->value) { - $parse->forward(); - do { - $implements[]= $parse->scope->resolve($parse->token->value); - $parse->forward(); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ('{' === $parse->token->value) { - break; - } else { - $parse->expecting(', or {', 'interfaces list'); - } - } while (null !== $parse->token->value); - } - - $parse->expecting('{', 'class'); - $body= $this->typeBody($parse); - $parse->expecting('}', 'class'); - - $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $parse->scope->annotations, $comment, $line); - $parse->scope->annotations= []; - return $return; - } - - public function expressions($parse, $end= ')') { - $arguments= []; - while ($end !== $parse->token->value) { - $arguments[]= $this->expression($parse, 0); - if (',' === $parse->token->value) { - $parse->forward(); - } else if ($end === $parse->token->value) { - break; - } else { - $parse->expecting($end.' or ,', 'argument list'); - break; - } - } - return $arguments; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/TransformationApi.class.php b/src/main/php/lang/ast/syntax/TransformationApi.class.php deleted file mode 100755 index 1a41dfb6..00000000 --- a/src/main/php/lang/ast/syntax/TransformationApi.class.php +++ /dev/null @@ -1,12 +0,0 @@ - $function) { - $emitter->transform($kind, $function); - } - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php deleted file mode 100755 index 7d3e6550..00000000 --- a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php +++ /dev/null @@ -1,23 +0,0 @@ -assertParsed( - [new Block([], self::LINE)], - '{ }' - ); - } - - #[@test] - public function with_invoke() { - $this->assertParsed( - [new Block([new InvokeExpression(new Literal('block', self::LINE), [], self::LINE)], self::LINE)], - '{ block(); }' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php deleted file mode 100755 index ee73a3fc..00000000 --- a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php +++ /dev/null @@ -1,72 +0,0 @@ -returns= new ReturnStatement( - new BinaryExpression( - new Variable('a', self::LINE), - '+', - new Literal('1', self::LINE), - self::LINE - ), - self::LINE - ); - } - - #[@test] - public function with_body() { - $this->assertParsed( - [new ClosureExpression(new Signature([], null), null, [$this->returns], self::LINE)], - 'function() { return $a + 1; };' - ); - } - - #[@test] - public function with_param() { - $params= [new Parameter('a', null, null, false, false, null, [])]; - $this->assertParsed( - [new ClosureExpression(new Signature($params, null), null, [$this->returns], self::LINE)], - 'function($a) { return $a + 1; };' - ); - } - - #[@test] - public function with_use_by_value() { - $this->assertParsed( - [new ClosureExpression(new Signature([], null), ['$a', '$b'], [$this->returns], self::LINE)], - 'function() use($a, $b) { return $a + 1; };' - ); - } - - #[@test] - public function with_use_by_reference() { - $this->assertParsed( - [new ClosureExpression(new Signature([], null), ['$a', '&$b'], [$this->returns], self::LINE)], - 'function() use($a, &$b) { return $a + 1; };' - ); - } - - #[@test] - public function with_return_type() { - $this->assertParsed( - [new ClosureExpression(new Signature([], new Type('int')), null, [$this->returns], self::LINE)], - 'function(): int { return $a + 1; };' - ); - } - - #[@test] - public function with_nullable_return_type() { - $this->assertParsed( - [new ClosureExpression(new Signature([], new Type('?int')), null, [$this->returns], self::LINE)], - 'function(): ?int { return $a + 1; };' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php b/src/test/php/lang/ast/unittest/parse/CommentTest.class.php deleted file mode 100755 index f610ce8f..00000000 --- a/src/test/php/lang/ast/unittest/parse/CommentTest.class.php +++ /dev/null @@ -1,87 +0,0 @@ -assertParsed([new Literal('"test"', 3)], ' - // This is a comment - "test"; - '); - } - - #[@test] - public function oneline_double_slash_at_end() { - $this->assertParsed([new Literal('"test"', 2)], ' - "test"; // This is a comment - '); - } - - #[@test] - public function two_oneline_double_slash() { - $this->assertParsed([new Literal('"test"', 4)], ' - // This is a comment - // This is another - "test"; - '); - } - - #[@test] - public function oneline_hashtag() { - $this->assertParsed([new Literal('"test"', 3)], ' - # This is a comment - "test"; - '); - } - - #[@test] - public function oneline_hashtag_at_end() { - $this->assertParsed([new Literal('"test"', 2)], ' - "test"; # This is a comment - '); - } - - #[@test] - public function two_oneline_hashtags() { - $this->assertParsed([new Literal('"test"', 4)], ' - # This is a comment - # This is another - "test"; - '); - } - - #[@test] - public function oneline_slash_asterisk() { - $this->assertParsed([new Literal('"test"', 3)], ' - /* This is a comment */ - "test"; - '); - } - - #[@test] - public function oneline_slash_asterisk_at_end() { - $this->assertParsed([new Literal('"test"', 2)], ' - "test"; /* This is a comment */ - '); - } - - #[@test] - public function oneline_slash_asterisk_inbetween() { - $this->assertParsed([new Literal('"before"', 2), new Literal('"after"', 2)], ' - "before"; /* This is a comment */ "after"; - '); - } - - #[@test] - public function multiline_slash_asterisk() { - $this->assertParsed([new Literal('"test"', 5)], ' - /* This is a comment - * spanning multiple lines. - */ - "test"; - '); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php deleted file mode 100755 index 2ff4c29d..00000000 --- a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php +++ /dev/null @@ -1,87 +0,0 @@ -blocks= [ - 1 => [new InvokeExpression(new Literal('action1', self::LINE), [], self::LINE)], - 2 => [new InvokeExpression(new Literal('action2', self::LINE), [], self::LINE)] - ]; - } - - #[@test] - public function plain_if() { - $this->assertParsed( - [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], null, self::LINE)], - 'if ($condition) { action1(); }' - ); - } - - #[@test] - public function if_with_else() { - $this->assertParsed( - [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], $this->blocks[2], self::LINE)], - 'if ($condition) { action1(); } else { action2(); }' - ); - } - - #[@test] - public function shortcut_if() { - $this->assertParsed( - [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], null, self::LINE)], - 'if ($condition) action1();' - ); - } - - #[@test] - public function shortcut_if_else() { - $this->assertParsed( - [new IfStatement(new Variable('condition', self::LINE), $this->blocks[1], $this->blocks[2], self::LINE)], - 'if ($condition) action1(); else action2();' - ); - } - - #[@test] - public function empty_switch() { - $this->assertParsed( - [new SwitchStatement(new Variable('condition', self::LINE), [], self::LINE)], - 'switch ($condition) { }' - ); - } - - #[@test] - public function switch_with_one_case() { - $cases= [new CaseLabel(new Literal('1', self::LINE), $this->blocks[1], self::LINE)]; - $this->assertParsed( - [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], - 'switch ($condition) { case 1: action1(); }' - ); - } - - #[@test] - public function switch_with_two_cases() { - $cases= [ - new CaseLabel(new Literal('1', self::LINE), $this->blocks[1], self::LINE), - new CaseLabel(new Literal('2', self::LINE), $this->blocks[2], self::LINE) - ]; - $this->assertParsed( - [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], - 'switch ($condition) { case 1: action1(); case 2: action2(); }' - ); - } - - #[@test] - public function switch_with_default() { - $cases= [new CaseLabel(null, $this->blocks[1], self::LINE)]; - $this->assertParsed( - [new SwitchStatement(new Variable('condition', self::LINE), $cases, self::LINE)], - 'switch ($condition) { default: action1(); }' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php deleted file mode 100755 index c71e73e0..00000000 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ /dev/null @@ -1,87 +0,0 @@ -fail('No exception raised', null, Errors::class); - } catch (Errors $expected) { - Assert::equals($message, $expected->getMessage()); - } - } - - #[@test] - public function missing_semicolon() { - $this->assertError( - 'Missing semicolon after assignment statement', - $this->parse('$a= 1 $b= 1;') - ); - } - - #[@test] - public function unclosed_brace_in_arguments() { - $this->assertError( - 'Expected ") or ,", have "(end)" in argument list', - $this->parse('call(') - ); - } - - #[@test] - public function unclosed_brace_in_parameters() { - $this->assertError( - 'Expected ",", have "(end)" in parameter list', - $this->parse('function($a') - ); - } - - #[@test] - public function unclosed_type() { - $this->assertError( - 'Expected a type, modifier, property, annotation, method or "}", have "-"', - $this->parse('class T { - }') - ); - } - - #[@test] - public function missing_comma_in_implements() { - $this->assertError( - 'Expected ", or {", have "B" in interfaces list', - $this->parse('class A implements I B { }') - ); - } - - #[@test] - public function missing_comma_in_interface_parents() { - $this->assertError( - 'Expected ", or {", have "B" in interface parents', - $this->parse('interface I extends A B { }') - ); - } - - #[@test] - public function unclosed_annotation() { - $this->assertError( - 'Expected ", or >>", have "(end)" in annotations', - $this->parse('<assertError( - 'Expected "]", have ";" in offset access', - $this->parse('$a[$s[0]= 5;') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php deleted file mode 100755 index 86cda762..00000000 --- a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php +++ /dev/null @@ -1,160 +0,0 @@ -assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [], self::LINE)], - 'function a() { }' - ); - } - - #[@test] - public function two_functions() { - $this->assertParsed( - [ - new FunctionDeclaration('a', new Signature([], null), [], self::LINE), - new FunctionDeclaration('b', new Signature([], null), [], self::LINE) - ], - 'function a() { } function b() { }' - ); - } - - #[@test, @values(['param', 'protected'])] - public function with_parameter($name) { - $params= [new Parameter($name, null, null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a($'.$name.') { }' - ); - } - - #[@test] - public function with_reference_parameter() { - $params= [new Parameter('param', null, null, true, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a(&$param) { }' - ); - } - - #[@test] - public function dangling_comma_in_parameter_lists() { - $params= [new Parameter('param', null, null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a($param, ) { }' - ); - } - - #[@test] - public function with_typed_parameter() { - $params= [new Parameter('param', new Type('string'), null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a(string $param) { }' - ); - } - - #[@test] - public function with_nullable_typed_parameter() { - $params= [new Parameter('param', new Type('?string'), null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a(?string $param) { }' - ); - } - - #[@test] - public function with_variadic_parameter() { - $params= [new Parameter('param', null, null, false, true, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a(... $param) { }' - ); - } - - #[@test] - public function with_optional_parameter() { - $params= [new Parameter('param', null, new Literal('null', self::LINE), false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a($param= null) { }' - ); - } - - #[@test] - public function with_parameter_named_function() { - $params= [new Parameter('function', null, null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a($function, ) { }' - ); - } - - #[@test] - public function with_typed_parameter_named_function() { - $params= [new Parameter('function', new FunctionType([], new Type('void')), null, false, false, null, [])]; - $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null), [], self::LINE)], - 'function a((function(): void) $function) { }' - ); - } - - #[@test] - public function with_return_type() { - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], new Type('void')), [], self::LINE)], - 'function a(): void { }' - ); - } - - #[@test] - public function with_nullable_return() { - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], new Type('?string')), [], self::LINE)], - 'function a(): ?string { }' - ); - } - - #[@test] - public function generator() { - $yield= new YieldExpression(null, null, self::LINE); - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], - 'function a() { yield; }' - ); - } - - #[@test] - public function generator_with_value() { - $yield= new YieldExpression(null, new Literal('1', self::LINE), self::LINE); - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], - 'function a() { yield 1; }' - ); - } - - #[@test] - public function generator_with_key_and_value() { - $yield= new YieldExpression(new Literal('"number"', self::LINE), new Literal('1', self::LINE), self::LINE); - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], - 'function a() { yield "number" => 1; }' - ); - } - - #[@test] - public function generator_delegation() { - $yield= new YieldFromExpression(new ArrayLiteral([], self::LINE), self::LINE); - $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null), [$yield], self::LINE)], - 'function a() { yield from []; }' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php deleted file mode 100755 index 46569a80..00000000 --- a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php +++ /dev/null @@ -1,56 +0,0 @@ -assertParsed( - [new InvokeExpression(new Literal('test', self::LINE), [], self::LINE)], - 'test();' - ); - } - - #[@test] - public function invoke_method() { - $instance= new InstanceExpression(new Variable('this', self::LINE), new Literal('test', self::LINE), self::LINE); - $this->assertParsed( - [new InvokeExpression($instance, [], self::LINE)], - '$this->test();' - ); - } - - #[@test] - public function invoke_function_with_argument() { - $arguments= [new Literal('1', self::LINE)]; - $this->assertParsed( - [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], - 'test(1);' - ); - } - - #[@test] - public function invoke_function_with_arguments() { - $arguments= [new Literal('1', self::LINE), new Literal('2', self::LINE)]; - $this->assertParsed( - [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], - 'test(1, 2);' - ); - } - - #[@test] - public function invoke_function_with_dangling_comma() { - $arguments= [new Literal('1', self::LINE), new Literal('2', self::LINE)]; - $this->assertParsed( - [new InvokeExpression(new Literal('test', self::LINE), $arguments, self::LINE)], - 'test(1, 2, );' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php deleted file mode 100755 index eb892dd5..00000000 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ /dev/null @@ -1,45 +0,0 @@ -expression= new BinaryExpression(new Variable('a', self::LINE), '+', new Literal('1', self::LINE), self::LINE); - } - - #[@test] - public function short_closure() { - $this->assertParsed( - [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], - 'fn($a) => $a + 1;' - ); - \xp::gc(); - } - - #[@test] - public function short_closure_as_arg() { - $this->assertParsed( - [new InvokeExpression( - new Literal('execute', self::LINE), - [new LambdaExpression(new Signature([new Parameter('a', null)], null), $this->expression, self::LINE)], - self::LINE - )], - 'execute(fn($a) => $a + 1);' - ); - \xp::gc(); - } - - #[@test] - public function short_closure_with_braces() { - $this->assertParsed( - [new LambdaExpression(new Signature([new Parameter('a', null)], null), [new ReturnStatement($this->expression, self::LINE)], self::LINE)], - 'fn($a) => { return $a + 1; };' - ); - \xp::gc(); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php deleted file mode 100755 index 8838f481..00000000 --- a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php +++ /dev/null @@ -1,84 +0,0 @@ -assertParsed([new Literal($input, self::LINE)], $input.';'); - } - - #[@test, @values(['0x00', '0x01', '0xFF', '0xff'])] - public function hexadecimal($input) { - $this->assertParsed([new Literal($input, self::LINE)], $input.';'); - } - - #[@test, @values(['00', '01', '010', '0777'])] - public function octal($input) { - $this->assertParsed([new Literal($input, self::LINE)], $input.';'); - } - - #[@test, @values(['1.0', '1.5'])] - public function decimal($input) { - $this->assertParsed([new Literal($input, self::LINE)], $input.';'); - } - - #[@test] - public function bool_true() { - $this->assertParsed([new Literal('true', self::LINE)], 'true;'); - } - - #[@test] - public function bool_false() { - $this->assertParsed([new Literal('false', self::LINE)], 'false;'); - } - - #[@test] - public function null() { - $this->assertParsed([new Literal('null', self::LINE)], 'null;'); - } - - #[@test] - public function empty_string() { - $this->assertParsed([new Literal('""', self::LINE)], '"";'); - } - - #[@test] - public function non_empty_string() { - $this->assertParsed([new Literal('"Test"', self::LINE)], '"Test";'); - } - - #[@test] - public function empty_array() { - $this->assertParsed([new ArrayLiteral([], self::LINE)], '[];'); - } - - #[@test] - public function int_array() { - $pairs= [ - [null, new Literal('1', self::LINE)], - [null, new Literal('2', self::LINE)] - ]; - $this->assertParsed([new ArrayLiteral($pairs, self::LINE)], '[1, 2];'); - } - - #[@test] - public function key_value_map() { - $pair= [new Literal('"key"', self::LINE), new Literal('"value"', self::LINE)]; - $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '["key" => "value"];'); - } - - #[@test] - public function dangling_comma_in_array() { - $pair= [null, new Literal('1', self::LINE)]; - $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '[1, ];'); - } - - #[@test] - public function dangling_comma_in_key_value_map() { - $pair= [new Literal('"key"', self::LINE), new Literal('"value"', self::LINE)]; - $this->assertParsed([new ArrayLiteral([$pair], self::LINE)], '["key" => "value", ];'); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php deleted file mode 100755 index ee6f488b..00000000 --- a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php +++ /dev/null @@ -1,158 +0,0 @@ -loop= new InvokeExpression(new Literal('loop', self::LINE), [], self::LINE); - } - - #[@test] - public function foreach_value() { - $this->assertParsed( - [new ForeachLoop( - new Variable('iterable', self::LINE), - null, - new Variable('value', self::LINE), - [$this->loop], - self::LINE - )], - 'foreach ($iterable as $value) { loop(); }' - ); - } - - #[@test] - public function foreach_key_value() { - $this->assertParsed( - [new ForeachLoop( - new Variable('iterable', self::LINE), - new Variable('key', self::LINE), - new Variable('value', self::LINE), - [$this->loop], - self::LINE - )], - 'foreach ($iterable as $key => $value) { loop(); }' - ); - } - - #[@test] - public function foreach_value_without_curly_braces() { - $this->assertParsed( - [new ForeachLoop( - new Variable('iterable', self::LINE), - null, - new Variable('value', self::LINE), - [$this->loop], - self::LINE - )], - 'foreach ($iterable as $value) loop();' - ); - } - - #[@test] - public function for_loop() { - $this->assertParsed( - [new ForLoop( - [new Assignment(new Variable('i', self::LINE), '=', new Literal('0', self::LINE), self::LINE)], - [new BinaryExpression(new Variable('i', self::LINE), '<', new Literal('10', self::LINE), self::LINE)], - [new UnaryExpression('suffix', new Variable('i', self::LINE), '++', self::LINE)], - [$this->loop], - self::LINE - )], - 'for ($i= 0; $i < 10; $i++) { loop(); }' - ); - } - - #[@test] - public function while_loop() { - $this->assertParsed( - [new WhileLoop( - new Variable('continue', self::LINE), - [$this->loop], - self::LINE - )], - 'while ($continue) { loop(); }' - ); - } - - #[@test] - public function while_loop_without_curly_braces() { - $this->assertParsed( - [new WhileLoop( - new Variable('continue', self::LINE), - [$this->loop], - self::LINE - )], - 'while ($continue) loop();' - ); - } - - #[@test] - public function do_loop() { - $this->assertParsed( - [new DoLoop( - new Variable('continue', self::LINE), - [$this->loop], - self::LINE - )], - 'do { loop(); } while ($continue);' - ); - } - - #[@test] - public function do_loop_without_curly_braces() { - $this->assertParsed( - [new DoLoop( - new Variable('continue', self::LINE), - [$this->loop], - self::LINE - )], - 'do loop(); while ($continue);' - ); - } - - #[@test] - public function break_statement() { - $this->assertParsed( - [new BreakStatement(null, self::LINE)], - 'break;' - ); - } - - #[@test] - public function break_statement_with_level() { - $this->assertParsed( - [new BreakStatement(new Literal('2', self::LINE), self::LINE)], - 'break 2;' - ); - } - - #[@test] - public function continue_statement() { - $this->assertParsed( - [new ContinueStatement(null, self::LINE)], - 'continue;' - ); - } - - #[@test] - public function continue_statement_with_level() { - $this->assertParsed( - [new ContinueStatement(new Literal('2', self::LINE), self::LINE)], - 'continue 2;' - ); - } - - #[@test] - public function goto_statement() { - $this->assertParsed( - [new Label('start', self::LINE), $this->loop, new GotoStatement('start', self::LINE)], - 'start: loop(); goto start;' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php deleted file mode 100755 index 44f4b0c0..00000000 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ /dev/null @@ -1,258 +0,0 @@ - new Property(['private'], 'a', null, null, [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private $a; }' - ); - } - - #[@test] - public function private_instance_properties() { - $body= [ - '$a' => new Property(['private'], 'a', null, null, [], null, self::LINE), - '$b' => new Property(['private'], 'b', null, null, [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private $a, $b; }' - ); - } - - #[@test] - public function private_instance_method() { - $body= [ - 'a()' => new Method(['private'], 'a', new Signature([], null), [], [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private function a() { } }' - ); - } - - #[@test] - public function private_static_method() { - $body= [ - 'a()' => new Method(['private', 'static'], 'a', new Signature([], null), [], [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private static function a() { } }' - ); - } - - #[@test] - public function class_constant() { - $body= [ - 'T' => new Constant([], 'T', null, new Literal('1', self::LINE), self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { const T = 1; }' - ); - } - - #[@test] - public function class_constants() { - $body= [ - 'T' => new Constant([], 'T', null, new Literal('1', self::LINE), self::LINE), - 'S' => new Constant([], 'S', null, new Literal('2', self::LINE), self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { const T = 1, S = 2; }' - ); - } - - #[@test] - public function private_class_constant() { - $body= [ - 'T' => new Constant(['private'], 'T', null, new Literal('1', self::LINE), self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private const T = 1; }' - ); - } - - #[@test] - public function method_with_return_type() { - $body= [ - 'a()' => new Method(['public'], 'a', new Signature([], new Type('void')), [], [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { public function a(): void { } }' - ); - } - - #[@test] - public function method_with_annotation() { - $annotations= ['test' => null]; - $body= [ - 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { <> public function a() { } }' - ); - } - - #[@test] - public function method_with_annotations() { - $annotations= ['test' => null, 'ignore' => new Literal('"Not implemented"', self::LINE)]; - $body= [ - 'a()' => new Method(['public'], 'a', new Signature([], null), [], $annotations, null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { <> public function a() { } }' - ); - } - - #[@test] - public function instance_property_access() { - $this->assertParsed( - [new InstanceExpression(new Variable('a', self::LINE), new Literal('member', self::LINE), self::LINE)], - '$a->member;' - ); - } - - #[@test] - public function dynamic_instance_property_access_via_variable() { - $this->assertParsed( - [new InstanceExpression(new Variable('a', self::LINE), new Variable('member', self::LINE), self::LINE)], - '$a->{$member};' - ); - } - - #[@test] - public function dynamic_instance_property_access_via_expression() { - $member= new InvokeExpression( - new InstanceExpression(new Variable('field', self::LINE), new Literal('get', self::LINE), self::LINE), - [new Variable('instance', self::LINE)], - self::LINE - ); - $this->assertParsed( - [new InstanceExpression(new Variable('a', self::LINE), $member, self::LINE)], - '$a->{$field->get($instance)};' - ); - } - - #[@test] - public function static_property_access() { - $this->assertParsed( - [new ScopeExpression('\\A', new Variable('member', self::LINE), self::LINE)], - 'A::$member;' - ); - } - - #[@test, @values(['self', 'parent', 'static'])] - public function scope_resolution($scope) { - $this->assertParsed( - [new ScopeExpression($scope, new Literal('class', self::LINE), self::LINE)], - $scope.'::class;' - ); - } - - #[@test] - public function class_resolution() { - $this->assertParsed( - [new ScopeExpression('\\A', new Literal('class', self::LINE), self::LINE)], - 'A::class;' - ); - } - - #[@test] - public function instance_method_invocation() { - $this->assertParsed( - [new InvokeExpression( - new InstanceExpression(new Variable('a', self::LINE), new Literal('member', self::LINE), self::LINE), - [new Literal('1', self::LINE)], - self::LINE - )], - '$a->member(1);' - ); - } - - #[@test] - public function static_method_invocation() { - $this->assertParsed( - [new ScopeExpression( - '\\A', - new InvokeExpression(new Literal('member', self::LINE), [new Literal('1', self::LINE)], self::LINE), - self::LINE - )], - 'A::member(1);' - ); - } - - #[@test] - public function typed_property() { - $body= [ - '$a' => new Property(['private'], 'a', new Type('string'), null, [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private string $a; }' - ); - } - - #[@test] - public function typed_property_with_value() { - $body= [ - '$a' => new Property(['private'], 'a', new Type('string'), new Literal('"test"', self::LINE), [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private string $a = "test"; }' - ); - } - - #[@test] - public function typed_properties() { - $body= [ - '$a' => new Property(['private'], 'a', new Type('string'), null, [], null, self::LINE), - '$b' => new Property(['private'], 'b', new Type('string'), null, [], null, self::LINE), - '$c' => new Property(['private'], 'c', new Type('int'), null, [], null, self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { private string $a, $b, int $c; }' - ); - } - - #[@test] - public function typed_constant() { - $body= [ - 'T' => new Constant([], 'T', new Type('int'), new Literal('1', self::LINE), self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { const int T = 1; }' - ); - } - - #[@test] - public function typed_constants() { - $body= [ - 'T' => new Constant([], 'T', new Type('int'), new Literal('1', self::LINE), self::LINE), - 'S' => new Constant([], 'S', new Type('int'), new Literal('2', self::LINE), self::LINE), - 'I' => new Constant([], 'I', new Type('string'), new Literal('"i"', self::LINE), self::LINE) - ]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { const int T = 1, S = 2, string I = "i"; }' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php deleted file mode 100755 index 531d9129..00000000 --- a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php +++ /dev/null @@ -1,55 +0,0 @@ -assertParsed( - [new NamespaceDeclaration('test', self::LINE)], - 'namespace test;' - ); - } - - #[@test] - public function compound_namespace() { - $this->assertParsed( - [new NamespaceDeclaration('lang\\ast', self::LINE)], - 'namespace lang\\ast;' - ); - } - - #[@test] - public function use_statement() { - $this->assertParsed( - [new UseStatement(null, ['lang\ast\Parse' => null], self::LINE)], - 'use lang\\ast\\Parse;' - ); - } - - #[@test] - public function use_with_alias() { - $this->assertParsed( - [new UseStatement(null, ['lang\ast\Parse' => 'P'], self::LINE)], - 'use lang\\ast\\Parse as P;' - ); - } - - #[@test] - public function grouped_use_statement() { - $this->assertParsed( - [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\Emitter' => null], self::LINE)], - 'use lang\\ast\\{Parse, Emitter};' - ); - } - - #[@test] - public function grouped_use_with_alias() { - $this->assertParsed( - [new UseStatement(null, ['lang\\ast\\Parse' => 'P'], self::LINE)], - 'use lang\\ast\\{Parse as P};' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php deleted file mode 100755 index c979722f..00000000 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ /dev/null @@ -1,313 +0,0 @@ ->', '<<' - #])] - public function binary($operator) { - $this->assertParsed( - [new BinaryExpression(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], - '$a '.$operator.' $b;' - ); - } - - #[@test] - public function ternary() { - $this->assertParsed( - [new TernaryExpression(new Variable('a', self::LINE), new Literal('1', self::LINE), new Literal('2', self::LINE), self::LINE)], - '$a ? 1 : 2;' - ); - } - - #[@test, @values([ - # '==', '!=', - # '===', '!==', - # '>', '>=', '<=', '<', '<=>' - #])] - public function comparison($operator) { - $this->assertParsed( - [new BinaryExpression(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], - '$a '.$operator.' $b;' - ); - } - - #[@test, @values(['++', '--'])] - public function suffix($operator) { - $this->assertParsed( - [new UnaryExpression('suffix', new Variable('a', self::LINE), $operator, self::LINE)], - '$a'.$operator.';' - ); - } - - #[@test, @values(['!', '~', '-', '+', '++', '--', '@', '&'])] - public function prefix($operator) { - $this->assertParsed( - [new UnaryExpression('prefix', new Variable('a', self::LINE), $operator, self::LINE)], - $operator.'$a;' - ); - } - - #[@test, @values([ - # '=', - # '+=', '-=', '*=', '/=', '.=', '**=', - # '&=', '|=', '^=', - # '>>=', '<<=' - #])] - public function assignment($operator) { - $this->assertParsed( - [new Assignment(new Variable('a', self::LINE), $operator, new Variable('b', self::LINE), self::LINE)], - '$a '.$operator.' $b;' - ); - } - - #[@test] - public function assignment_to_offset() { - $target= new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE); - $this->assertParsed( - [new Assignment($target, '=', new Variable('b', self::LINE), self::LINE)], - '$a[0]= $b;' - ); - } - - #[@test] - public function destructuring_assignment() { - $target= new ArrayLiteral([[null, new Variable('a', self::LINE)], [null, new Variable('b', self::LINE)]], self::LINE); - $this->assertParsed( - [new Assignment($target, '=', new Variable('c', self::LINE), self::LINE)], - '[$a, $b]= $c;' - ); - } - - #[@test] - public function comparison_to_assignment() { - $this->assertParsed( - [new BinaryExpression( - new Literal('1', self::LINE), '===', new Braced( - new Assignment(new Variable('a', self::LINE), '=', new Literal('1', self::LINE), self::LINE), - self::LINE - ), - self::LINE - )], - '1 === ($a= 1);' - ); - } - - #[@test] - public function append_array() { - $target= new OffsetExpression(new Variable('a', self::LINE), null, self::LINE); - $this->assertParsed( - [new Assignment($target, '=', new Variable('b', self::LINE), self::LINE)], - '$a[]= $b;' - ); - } - - #[@test] - public function clone_expression() { - $this->assertParsed( - [new UnaryExpression('prefix', new Variable('a', self::LINE), 'clone', self::LINE)], - 'clone $a;' - ); - } - - #[@test] - public function error_suppression() { - $this->assertParsed( - [new UnaryExpression('prefix', new Variable('a', self::LINE), '@', self::LINE)], - '@$a;' - ); - } - - #[@test] - public function reference() { - $this->assertParsed( - [new UnaryExpression('prefix', new Variable('a', self::LINE), '&', self::LINE)], - '&$a;' - ); - } - - #[@test] - public function new_type() { - $this->assertParsed( - [new NewExpression('\\T', [], self::LINE)], - 'new T();' - ); - } - - #[@test] - public function new_var() { - $this->assertParsed( - [new NewExpression('$class', [], self::LINE)], - 'new $class();' - ); - } - - #[@test] - public function new_expr() { - $this->assertParsed( - [new NewExpression(new InvokeExpression(new Literal('factory', self::LINE), [], self::LINE), [], self::LINE)], - 'new (factory())();' - ); - } - - #[@test] - public function new_type_with_args() { - $this->assertParsed( - [new NewExpression('\\T', [new Variable('a', self::LINE), new Variable('b', self::LINE)], self::LINE)], - 'new T($a, $b);' - ); - } - - #[@test] - public function new_anonymous_extends() { - $declaration= new ClassDeclaration([], null, '\\T', [], [], [], null, self::LINE); - $this->assertParsed( - [new NewClassExpression($declaration, [], self::LINE)], - 'new class() extends T { };' - ); - } - - #[@test] - public function new_anonymous_implements() { - $declaration= new ClassDeclaration([], null, null, ['\\A', '\\B'], [], [], null, self::LINE); - $this->assertParsed( - [new NewClassExpression($declaration, [], self::LINE)], - 'new class() implements A, B { };' - ); - } - - #[@test] - public function scope_resolution_operator() { - $this->assertParsed( - [new ScopeExpression('\\Objects', new Literal('ID', self::LINE), self::LINE)], - 'Objects::ID;' - ); - } - - #[@test] - public function precedence_of_object_operator_binary() { - $this->assertParsed( - [new BinaryExpression( - new InstanceExpression(new Variable('this', self::LINE), new Literal('a', self::LINE), self::LINE), - '.', - new Literal('"test"', self::LINE), - self::LINE - )], - '$this->a."test";' - ); - } - - #[@test] - public function precedence_of_object_operator_unary() { - $this->assertParsed( - [new UnaryExpression( - 'prefix', - new InstanceExpression(new Variable('this', self::LINE), new Literal('a', self::LINE), self::LINE), - '!', - self::LINE - )], - '!$this->a;' - ); - } - - #[@test] - public function precedence_of_scope_resolution_operator_binary() { - $this->assertParsed( - [new BinaryExpression( - new ScopeExpression('self', new Literal('class', self::LINE), self::LINE), - '.', - new Literal('"test"', self::LINE), - self::LINE - )], - 'self::class."test";' - ); - } - - #[@test] - public function precedence_of_scope_resolution_operator_unary() { - $this->assertParsed( - [new UnaryExpression( - 'prefix', - new ScopeExpression('\\Objects', new Literal('ID', self::LINE), self::LINE), - '!', - self::LINE - )], - '!Objects::ID;' - ); - } - - #[@test] - public function precedence_of_not_and_instance_of() { - $this->assertParsed( - [new UnaryExpression( - 'prefix', - new InstanceOfExpression(new Variable('this', self::LINE), 'self', self::LINE), - '!', - self::LINE - )], - '!$this instanceof self;' - ); - } - - #[@test, @values(['+', '-', '~'])] - public function precedence_of_prefix($operator) { - $this->assertParsed( - [new BinaryExpression( - new UnaryExpression('prefix', new Literal('2', self::LINE), $operator, self::LINE), - '===', - new Variable('value', self::LINE), - self::LINE - )], - $operator.'2 === $value;' - ); - } - - #[@test] - public function precedence_of_braces_unary() { - $this->assertParsed( - [new UnaryExpression( - 'prefix', - new InvokeExpression(new Variable('a', self::LINE), [], self::LINE), - '!', - self::LINE - )], - '!$a();' - ); - } - - #[@test] - public function precedence_of_offset_unary() { - $this->assertParsed( - [new UnaryExpression( - 'prefix', - new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE), - '!', - self::LINE - )], - '!$a[0];' - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php deleted file mode 100755 index cfba572f..00000000 --- a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php +++ /dev/null @@ -1,37 +0,0 @@ -execute(); - } - - /** - * Assertion helper - * - * @param [:var][] $expected - * @param iterable $nodes - * @throws unittest.AssertionFailedError - * @return void - */ - protected function assertParsed($expected, $code) { - $actual= []; - foreach ($this->parse($code) as $node) { - $actual[]= $node; - } - Assert::equals($expected, $actual); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php deleted file mode 100755 index e04c905c..00000000 --- a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php +++ /dev/null @@ -1,23 +0,0 @@ -assertParsed( - [new Start('php', self::LINE), new NamespaceDeclaration('test', self::LINE)], - 'assertParsed( - [new Start('hh', self::LINE), new NamespaceDeclaration('test', self::LINE)], - 'assertParsed( - [new ClassDeclaration([], '\\A', null, [], [], [], null, self::LINE)], - 'class A { }' - ); - } - - #[@test] - public function class_with_parent() { - $this->assertParsed( - [new ClassDeclaration([], '\\A', '\\B', [], [], [], null, self::LINE)], - 'class A extends B { }' - ); - } - - #[@test] - public function class_with_interface() { - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, ['\\C'], [], [], null, self::LINE)], - 'class A implements C { }' - ); - } - - #[@test] - public function class_with_interfaces() { - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, ['\\C', '\\D'], [], [], null, self::LINE)], - 'class A implements C, D { }' - ); - } - - #[@test] - public function abstract_class() { - $this->assertParsed( - [new ClassDeclaration(['abstract'], '\\A', null, [], [], [], null, self::LINE)], - 'abstract class A { }' - ); - } - - #[@test] - public function final_class() { - $this->assertParsed( - [new ClassDeclaration(['final'], '\\A', null, [], [], [], null, self::LINE)], - 'final class A { }' - ); - } - - #[@test] - public function empty_interface() { - $this->assertParsed( - [new InterfaceDeclaration([], '\\A', [], [], [], null, self::LINE)], - 'interface A { }' - ); - } - - #[@test] - public function interface_with_parent() { - $this->assertParsed( - [new InterfaceDeclaration([], '\\A', ['\\B'], [], [], null, self::LINE)], - 'interface A extends B { }' - ); - } - - #[@test] - public function interface_with_parents() { - $this->assertParsed( - [new InterfaceDeclaration([], '\\A', ['\\B', '\\C'], [], [], null, self::LINE)], - 'interface A extends B, C { }' - ); - } - - #[@test] - public function empty_trait() { - $this->assertParsed( - [new TraitDeclaration([], '\\A', [], [], null, self::LINE)], - 'trait A { }' - ); - } - - #[@test] - public function class_with_trait() { - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], [new UseExpression(['\\B'], [], self::LINE)], [], null, self::LINE)], - 'class A { use B; }' - ); - } - - #[@test] - public function class_with_multiple_traits() { - $body= [new UseExpression(['\\B'], [], self::LINE), new UseExpression(['\\C'], [], self::LINE)]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { use B; use C; }' - ); - } - - #[@test] - public function class_with_comma_separated_traits() { - $body= [new UseExpression(['\\B', '\\C'], [], self::LINE)]; - $this->assertParsed( - [new ClassDeclaration([], '\\A', null, [], $body, [], null, self::LINE)], - 'class A { use B, C; }' - ); - } - - #[@test] - public function class_in_namespace() { - $this->assertParsed( - [new NamespaceDeclaration('test', self::LINE), new ClassDeclaration([], '\\test\\A', null, [], [], [], null, self::LINE)], - 'namespace test; class A { }' - ); - } - - #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare method b()'])] - public function cannot_redeclare_method() { - iterator_to_array($this->parse('class A { public function b() { } public function b() { }}')); - } - - #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare property $b'])] - public function cannot_redeclare_property() { - iterator_to_array($this->parse('class A { public $b; private $b; }')); - } - - #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Cannot redeclare constant B'])] - public function cannot_redeclare_constant() { - iterator_to_array($this->parse('class A { const B = 1; const B = 3; }')); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php deleted file mode 100755 index 40c51aa6..00000000 --- a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php +++ /dev/null @@ -1,49 +0,0 @@ -assertParsed( - [new Variable($name, self::LINE)], - '$'.$name.';' - ); - } - - #[@test] - public function static_variable() { - $this->assertParsed( - [new StaticLocals(['v' => null], self::LINE)], - 'static $v;' - ); - } - - #[@test] - public function static_variable_with_initialization() { - $this->assertParsed( - [new StaticLocals(['id' => new Literal('0', self::LINE)], self::LINE)], - 'static $id= 0;' - ); - } - - #[@test] - public function array_offset() { - $this->assertParsed( - [new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE)], - '$a[0];' - ); - } - - /** @deprecated */ - #[@test] - public function string_offset() { - $this->assertParsed( - [new OffsetExpression(new Variable('a', self::LINE), new Literal('0', self::LINE), self::LINE)], - '$a{0};' - ); - \xp::gc(); - } -} \ No newline at end of file From bc3245104a7ea7da7bfdd934dbcb53c3773b22f4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 16:18:19 +0200 Subject: [PATCH 225/926] Bump dependencies --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index dc9b39dd..5b137daa 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "^4.0 | ^3.0", + "xp-framework/ast": "dev-refactor/integrate-parser", "php" : ">=7.0.0" }, "require-dev" : { From 9bf9d1dee8be92ab6fc452955221bff4312767bd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 16:18:29 +0200 Subject: [PATCH 226/926] Ignore test for the moment --- .../php/lang/ast/unittest/emit/PropertyTypesTest.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index 9c5d99c4..171abd34 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -1,6 +1,7 @@ getField('test')->getTypeName()); } - #[@test] + #[@test, @ignore('Resolved to for the moment, resolve() concept needs fixing!')] public function self_type() { $t= $this->type('class { private static self $instance; From c81843674569bda4b5ca0134ad0147199a9d20aa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 16:24:18 +0200 Subject: [PATCH 227/926] QA: Update link to implemented property types RFC --- src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index 171abd34..046826d2 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -6,7 +6,7 @@ * Property types * * @see https://docs.hhvm.com/hack/types/type-system - * @see https://wiki.php.net/rfc/property_type_hints (Draft) + * @see https://wiki.php.net/rfc/typed_properties_v2 */ class PropertyTypesTest extends EmittingTest { From 56339acfe407ad7ff5e9feece4ab1c65521c8e8c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 17:27:04 +0200 Subject: [PATCH 228/926] Un-ignore self_type() test again, fixed by xp-framework/ast@d09a309 --- src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index 046826d2..e6f7b38b 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -19,7 +19,7 @@ public function int_type() { Assert::equals('int', $t->getField('test')->getTypeName()); } - #[@test, @ignore('Resolved to for the moment, resolve() concept needs fixing!')] + #[@test] public function self_type() { $t= $this->type('class { private static self $instance; From 8f54e1dd402049da4cfd25fedc718759b867ebc0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 22:06:43 +0200 Subject: [PATCH 229/926] Rename Parse::execute() to Parse::stream() --- src/main/php/lang/ast/Compiled.class.php | 5 ++--- src/main/php/xp/compiler/AstRunner.class.php | 4 ++-- .../php/xp/compiler/CompileRunner.class.php | 4 ++-- .../ast/unittest/LineNumberTest.class.php | 19 ++++++++--------- .../lang/ast/unittest/TokensTest.class.php | 21 +++++++++---------- .../ast/unittest/emit/EmittingTest.class.php | 5 ++--- 6 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index efb787f7..9fb32c84 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -1,7 +1,6 @@ emitAll(new Result($out), $parse->execute()); + $parse= new Parse($language, new Tokens($in, $file)); + self::$emit[$version]->emitAll(new Result($out), $parse->stream()); return $out; } finally { $in->close(); diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index eddede3e..07122fb5 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -88,8 +88,8 @@ public static function main(array $args) { Console::writeLinef("\033[1m══ %s ══%s\033[0m", $file, str_repeat('═', 72 - 6 - strlen($file))); try { - $parse= new Parse($lang, new Tokens(new StreamTokenizer($in)), $file); - foreach ($parse->execute() as $node) { + $parse= new Parse($lang, new Tokens($in, $file)); + foreach ($parse->stream() as $node) { Console::writeLine(self::stringOf($node)); } } catch (Errors $e) { diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index a74a94e6..73005fef 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -77,8 +77,8 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= new Parse($lang, new Tokens(new StreamTokenizer($in)), $file); - $emit->emitAll(new Result($output->target((string)$path)), $parse->execute()); + $parse= new Parse($lang, new Tokens($in, $file)); + $emit->emitAll(new Result($output->target((string)$path)), $parse->stream()); $t->stop(); Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); diff --git a/src/test/php/lang/ast/unittest/LineNumberTest.class.php b/src/test/php/lang/ast/unittest/LineNumberTest.class.php index 8591fa90..46795bda 100755 --- a/src/test/php/lang/ast/unittest/LineNumberTest.class.php +++ b/src/test/php/lang/ast/unittest/LineNumberTest.class.php @@ -1,7 +1,6 @@ assertPositions( [['HERE' => 1]], - new Tokens(new StringTokenizer("HERE")) + new Tokens("HERE") ); } @@ -34,7 +33,7 @@ public function starts_with_line_number_one() { public function unix_lines() { $this->assertPositions( [['LINE1' => 1], ['LINE2' => 2]], - new Tokens(new StringTokenizer("LINE1\nLINE2")) + new Tokens("LINE1\nLINE2") ); } @@ -42,7 +41,7 @@ public function unix_lines() { public function windows_lines() { $this->assertPositions( [['LINE1' => 1], ['LINE2' => 2]], - new Tokens(new StringTokenizer("LINE1\r\nLINE2")) + new Tokens("LINE1\r\nLINE2") ); } @@ -50,7 +49,7 @@ public function windows_lines() { public function after_regular_comment() { $this->assertPositions( [['HERE' => 2]], - new Tokens(new StringTokenizer("// Comment\nHERE")) + new Tokens("// Comment\nHERE") ); } @@ -58,7 +57,7 @@ public function after_regular_comment() { public function apidoc_comment() { $this->assertPositions( [['COMMENT' => 1], ['HERE' => 2]], - new Tokens(new StringTokenizer("/** COMMENT */\nHERE")) + new Tokens("/** COMMENT */\nHERE") ); } @@ -66,7 +65,7 @@ public function apidoc_comment() { public function multi_line_apidoc_comment() { $this->assertPositions( [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** LINE1\nLINE2 */\nHERE")) + new Tokens("/** LINE1\nLINE2 */\nHERE") ); } @@ -74,7 +73,7 @@ public function multi_line_apidoc_comment() { public function multi_line_apidoc_comment_is_trimmed() { $this->assertPositions( [['COMMENT' => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** COMMENT\n */\nHERE")) + new Tokens("/** COMMENT\n */\nHERE") ); } @@ -82,7 +81,7 @@ public function multi_line_apidoc_comment_is_trimmed() { public function multi_line_apidoc_comment_leading_stars_removed() { $this->assertPositions( [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** LINE1\n * LINE2 */\nHERE")) + new Tokens("/** LINE1\n * LINE2 */\nHERE") ); } @@ -90,7 +89,7 @@ public function multi_line_apidoc_comment_leading_stars_removed() { public function multi_line_string() { $this->assertPositions( [["'STRING\n'" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("'STRING\n'\nHERE")) + new Tokens("'STRING\n'\nHERE") ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php index 61b2f779..7043036c 100755 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ b/src/test/php/lang/ast/unittest/TokensTest.class.php @@ -2,7 +2,6 @@ use lang\FormatException; use lang\ast\Tokens; -use text\StringTokenizer; use unittest\Assert; class TokensTest { @@ -25,7 +24,7 @@ private function assertTokens($expected, $tokens) { #[@test] public function can_create() { - new Tokens(new StringTokenizer('test')); + new Tokens('test'); } #[@test, @values([ @@ -38,7 +37,7 @@ public function can_create() { # "'\\\\\\''", #])] public function string_literals($input) { - $this->assertTokens([['string' => $input]], new Tokens(new StringTokenizer($input))); + $this->assertTokens([['string' => $input]], new Tokens($input)); } #[@test, @expect(['class' => FormatException::class, 'withMessage' => '/Unclosed string literal/']), @values([ @@ -48,18 +47,18 @@ public function string_literals($input) { # "'Test" #])] public function unclosed_string_literals($input) { - $t= (new Tokens(new StringTokenizer($input)))->getIterator(); + $t= (new Tokens($input))->getIterator(); $t->current(); } #[@test, @values(['0', '1', '1_000_000_000'])] public function integer_literal($input) { - $this->assertTokens([['integer' => str_replace('_', '', $input)]], new Tokens(new StringTokenizer($input))); + $this->assertTokens([['integer' => str_replace('_', '', $input)]], new Tokens($input)); } #[@test, @values(['0.0', '6.1', '.5', '107_925_284.88'])] public function float_literal($input) { - $this->assertTokens([['decimal' => str_replace('_', '', $input)]], new Tokens(new StringTokenizer($input))); + $this->assertTokens([['decimal' => str_replace('_', '', $input)]], new Tokens($input)); } #[@test, @values([ @@ -68,7 +67,7 @@ public function float_literal($input) { # '$input' #])] public function variables($input) { - $this->assertTokens([['variable' => substr($input, 1)]], new Tokens(new StringTokenizer($input))); + $this->assertTokens([['variable' => substr($input, 1)]], new Tokens($input)); } #[@test, @values([ @@ -80,24 +79,24 @@ public function variables($input) { # '->', #])] public function operators($input) { - $this->assertTokens([['operator' => $input]], new Tokens(new StringTokenizer($input))); + $this->assertTokens([['operator' => $input]], new Tokens($input)); } #[@test] public function annotation() { $this->assertTokens( [['operator' => '<<'], ['name' => 'test'], ['operator' => '>>']], - new Tokens(new StringTokenizer('<>')) + new Tokens('<>') ); } #[@test] public function regular_comment() { - $this->assertTokens([], new Tokens(new StringTokenizer('// Comment'))); + $this->assertTokens([], new Tokens('// Comment')); } #[@test] public function apidoc_comment() { - $this->assertTokens([['comment' => 'Test']], new Tokens(new StringTokenizer('/** Test */'))); + $this->assertTokens([['comment' => 'Test']], new Tokens('/** Test */')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index c05a4c91..303496ec 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -3,7 +3,6 @@ use io\streams\{MemoryOutputStream, StringWriter}; use lang\DynamicClassLoader; use lang\ast\{CompilingClassLoader, Emitter, Language, Node, Parse, Result, Tokens}; -use text\StringTokenizer; use unittest\Assert; use unittest\TestCase; use util\cmd\Console; @@ -56,8 +55,8 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= new Parse($this->language, new Tokens(new StringTokenizer(str_replace('', $name, $code))), static::class); - $ast= iterator_to_array($parse->execute()); + $parse= new Parse($this->language, new Tokens(str_replace('', $name, $code), static::class)); + $ast= iterator_to_array($parse->stream()); if (isset($this->output['ast'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); From 967c1080da28c088bd1971d3fb4515015fc399fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 22:27:15 +0200 Subject: [PATCH 230/926] Use Language::parse() --- src/main/php/lang/ast/Compiled.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 9fb32c84..5a868edf 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -27,8 +27,7 @@ private static function parse($lang, $in, $version, $out, $file) { ; try { - $parse= new Parse($language, new Tokens($in, $file)); - self::$emit[$version]->emitAll(new Result($out), $parse->stream()); + self::$emit[$version]->emitAll(new Result($out), $language->parse(new Tokens($in, $file))->stream()); return $out; } finally { $in->close(); From 6de6d0ff5058e4baa41db2dc3bd6fbc2457abc41 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 22:32:01 +0200 Subject: [PATCH 231/926] Use Language::parse() instead of constructing Parse class directly --- src/main/php/xp/compiler/AstRunner.class.php | 12 ++---------- src/main/php/xp/compiler/CompileRunner.class.php | 4 ++-- .../lang/ast/unittest/emit/EmittingTest.class.php | 4 ++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index 07122fb5..788b9f47 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -1,13 +1,6 @@ stream() as $node) { + foreach ($lang->parse(new Tokens($in, $file))->stream() as $node) { Console::writeLine(self::stringOf($node)); } } catch (Errors $e) { diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 73005fef..6fa30e50 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -2,7 +2,7 @@ use io\Path; use lang\Runtime; -use lang\ast\{CompilingClassloader, Emitter, Errors, Language, Parse, Result, Tokens}; +use lang\ast\{CompilingClassloader, Emitter, Errors, Language, Result, Tokens}; use text\StreamTokenizer; use util\cmd\Console; use util\profiling\Timer; @@ -77,7 +77,7 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= new Parse($lang, new Tokens($in, $file)); + $parse= $lang->parse(new Tokens($in, $file)); $emit->emitAll(new Result($output->target((string)$path)), $parse->stream()); $t->stop(); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 303496ec..ab072ba8 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -2,7 +2,7 @@ use io\streams\{MemoryOutputStream, StringWriter}; use lang\DynamicClassLoader; -use lang\ast\{CompilingClassLoader, Emitter, Language, Node, Parse, Result, Tokens}; +use lang\ast\{CompilingClassLoader, Emitter, Language, Result, Tokens}; use unittest\Assert; use unittest\TestCase; use util\cmd\Console; @@ -55,7 +55,7 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= new Parse($this->language, new Tokens(str_replace('', $name, $code), static::class)); + $parse= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class)); $ast= iterator_to_array($parse->stream()); if (isset($this->output['ast'])) { Console::writeLine(); From a5a96431ccc1bca9f7b3a5030e3b6b72deb11ded Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 May 2020 22:33:24 +0200 Subject: [PATCH 232/926] Remove HHVM leftover --- src/main/php/xp/compiler/AstRunner.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index 788b9f47..00b738ab 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -69,7 +69,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(); + $emit= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } From d2a1b652102991fc62bc2d0b4db03a14fa8b1fd7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 3 May 2020 17:05:50 +0200 Subject: [PATCH 233/926] Move to AST library --- .../ast/unittest/LineNumberTest.class.php | 95 ---------------- .../lang/ast/unittest/TokensTest.class.php | 102 ------------------ 2 files changed, 197 deletions(-) delete mode 100755 src/test/php/lang/ast/unittest/LineNumberTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/TokensTest.class.php diff --git a/src/test/php/lang/ast/unittest/LineNumberTest.class.php b/src/test/php/lang/ast/unittest/LineNumberTest.class.php deleted file mode 100755 index 46795bda..00000000 --- a/src/test/php/lang/ast/unittest/LineNumberTest.class.php +++ /dev/null @@ -1,95 +0,0 @@ - $value) { - $actual[]= [$value[0] => $value[1]]; - } - Assert::equals($expected, $actual); - } - - #[@test] - public function starts_with_line_number_one() { - $this->assertPositions( - [['HERE' => 1]], - new Tokens("HERE") - ); - } - - #[@test] - public function unix_lines() { - $this->assertPositions( - [['LINE1' => 1], ['LINE2' => 2]], - new Tokens("LINE1\nLINE2") - ); - } - - #[@test] - public function windows_lines() { - $this->assertPositions( - [['LINE1' => 1], ['LINE2' => 2]], - new Tokens("LINE1\r\nLINE2") - ); - } - - #[@test] - public function after_regular_comment() { - $this->assertPositions( - [['HERE' => 2]], - new Tokens("// Comment\nHERE") - ); - } - - #[@test] - public function apidoc_comment() { - $this->assertPositions( - [['COMMENT' => 1], ['HERE' => 2]], - new Tokens("/** COMMENT */\nHERE") - ); - } - - #[@test] - public function multi_line_apidoc_comment() { - $this->assertPositions( - [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens("/** LINE1\nLINE2 */\nHERE") - ); - } - - #[@test] - public function multi_line_apidoc_comment_is_trimmed() { - $this->assertPositions( - [['COMMENT' => 1], ['HERE' => 3]], - new Tokens("/** COMMENT\n */\nHERE") - ); - } - - #[@test] - public function multi_line_apidoc_comment_leading_stars_removed() { - $this->assertPositions( - [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens("/** LINE1\n * LINE2 */\nHERE") - ); - } - - #[@test] - public function multi_line_string() { - $this->assertPositions( - [["'STRING\n'" => 1], ['HERE' => 3]], - new Tokens("'STRING\n'\nHERE") - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php deleted file mode 100755 index 7043036c..00000000 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ /dev/null @@ -1,102 +0,0 @@ - $value) { - $actual[]= [$type => $value[0]]; - } - Assert::equals($expected, $actual); - } - - #[@test] - public function can_create() { - new Tokens('test'); - } - - #[@test, @values([ - # '""', - # "''", - # "'\\\\'", - # '"Test"', - # "'Test'", - # "'Test\''", - # "'\\\\\\''", - #])] - public function string_literals($input) { - $this->assertTokens([['string' => $input]], new Tokens($input)); - } - - #[@test, @expect(['class' => FormatException::class, 'withMessage' => '/Unclosed string literal/']), @values([ - # '"', - # "'", - # '"Test', - # "'Test" - #])] - public function unclosed_string_literals($input) { - $t= (new Tokens($input))->getIterator(); - $t->current(); - } - - #[@test, @values(['0', '1', '1_000_000_000'])] - public function integer_literal($input) { - $this->assertTokens([['integer' => str_replace('_', '', $input)]], new Tokens($input)); - } - - #[@test, @values(['0.0', '6.1', '.5', '107_925_284.88'])] - public function float_literal($input) { - $this->assertTokens([['decimal' => str_replace('_', '', $input)]], new Tokens($input)); - } - - #[@test, @values([ - # '$a', - # '$_', - # '$input' - #])] - public function variables($input) { - $this->assertTokens([['variable' => substr($input, 1)]], new Tokens($input)); - } - - #[@test, @values([ - # '+', '-', '*', '/', '**', - # '==', '!=', - # '<=', '>=', '<=>', - # '===', '!==', - # '=>', - # '->', - #])] - public function operators($input) { - $this->assertTokens([['operator' => $input]], new Tokens($input)); - } - - #[@test] - public function annotation() { - $this->assertTokens( - [['operator' => '<<'], ['name' => 'test'], ['operator' => '>>']], - new Tokens('<>') - ); - } - - #[@test] - public function regular_comment() { - $this->assertTokens([], new Tokens('// Comment')); - } - - #[@test] - public function apidoc_comment() { - $this->assertTokens([['comment' => 'Test']], new Tokens('/** Test */')); - } -} \ No newline at end of file From d2054b5a532727be1f40a5b93a9c8f8c6a01581f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 3 May 2020 17:07:30 +0200 Subject: [PATCH 234/926] Use parse tree API --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index ab072ba8..bc078e72 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -55,15 +55,14 @@ protected function type($code) { $name= 'T'.(self::$id++); $out= new MemoryOutputStream(); - $parse= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class)); - $ast= iterator_to_array($parse->stream()); + $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); if (isset($this->output['ast'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); - Console::writeLine($ast); + Console::writeLine($tree); } - $this->emitter->emitAll(new Result(new StringWriter($out)), $ast); + $this->emitter->emitAll(new Result(new StringWriter($out)), $tree->children()); if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); From cbc94b286cfe670b5df3c96a84193c5ccd1d6d20 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 3 May 2020 17:08:56 +0200 Subject: [PATCH 235/926] Use MemoryOutputStream::bytes() --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index bc078e72..cedb3e72 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -66,10 +66,10 @@ protected function type($code) { if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); - Console::writeLine($out->getBytes()); + Console::writeLine($out->bytes()); } - $this->cl->setClassBytes($name, $out->getBytes()); + $this->cl->setClassBytes($name, $out->bytes()); return $this->cl->loadClass($name); } From 17c8d4eb7f22f1c6773c7b2ba05d7db7e1d7ba54 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 15:03:33 +0200 Subject: [PATCH 236/926] Use declare() instead of deprecated inject() --- .../php/lang/ast/unittest/emit/TransformationsTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 241ef73e..a51a393b 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -11,7 +11,7 @@ class TransformationsTest extends EmittingTest { public function setUp() { $this->transform('class', function($codegen, $class) { if ($class->annotation('repr')) { - $class->inject(new Method( + $class->declare(new Method( ['public'], 'toString', new Signature([], new Type('string')), @@ -23,7 +23,7 @@ public function setUp() { $this->transform('class', function($codegen, $class) { if ($class->annotation('getters')) { foreach ($class->properties() as $property) { - $class->inject(new Method( + $class->declare(new Method( ['public'], $property->name, new Signature([], $property->type), From 1aa8afbc66ed285a80f2c07bf9e7c4b91ed303b7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 15:04:02 +0200 Subject: [PATCH 237/926] Adjust to type system refactoring --- src/main/php/lang/ast/emit/PHP.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d96e99e4..531c3fce 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,8 @@ literal() && 'void' !== $type->literal()) { return $type->literal(); From 19dab32697cfad6ac994e99b81033372cc30b6c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 17:55:50 +0200 Subject: [PATCH 238/926] Bump dependency on xp-framework/ast after PR has been merged --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5b137daa..5c61b13a 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-refactor/integrate-parser", + "xp-framework/ast": "dev-master as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From ca4470537dd57078bb7258d9271ba48fd50cc051 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 17:56:00 +0200 Subject: [PATCH 239/926] Document parser has been extracted --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index ba7bcc3c..b54e1926 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #84: Extract parser - @thekid + ## 5.1.3 / 2020-04-04 * Allowed `::class` on objects (PHP 8.0 forward compatibility) - @thekid From 054f4586f3d8e5100cbfba38d1cfe8d0121e5805 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 18:32:27 +0200 Subject: [PATCH 240/926] Verify `insteadof` trait keyword works --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- .../php/lang/ast/unittest/emit/Loading.class.php | 2 +- .../php/lang/ast/unittest/emit/Spinner.class.php | 7 +++++++ .../php/lang/ast/unittest/emit/TraitsTest.class.php | 13 +++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/Spinner.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 531c3fce..980d2474 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -361,7 +361,7 @@ protected function emitUse($result, $use) { if ($use->aliases) { $result->out->write('{'); foreach ($use->aliases as $reference => $alias) { - $result->out->write($reference.' as '.$alias.';'); + $result->out->write($reference.' '.key($alias).' '.current($alias).';'); } $result->out->write('}'); } else { diff --git a/src/test/php/lang/ast/unittest/emit/Loading.class.php b/src/test/php/lang/ast/unittest/emit/Loading.class.php index e788f1d6..c7f17ad9 100755 --- a/src/test/php/lang/ast/unittest/emit/Loading.class.php +++ b/src/test/php/lang/ast/unittest/emit/Loading.class.php @@ -2,6 +2,6 @@ trait Loading { - public function loaded() { } + public function loaded() { return 'Loaded'; } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/Spinner.class.php b/src/test/php/lang/ast/unittest/emit/Spinner.class.php new file mode 100755 index 00000000..455d2276 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/Spinner.class.php @@ -0,0 +1,7 @@ +hasMethod('hasLoaded')); } + + #[@test] + public function trait_method_insteadof() { + $t= $this->type('use lang\ast\unittest\emit\{Loading, Spinner}; class { + use Loading, Spinner { + Spinner::loaded as noLongerSpinning; + Loading::loaded insteadof Spinner; + } + }'); + $instance= $t->newInstance(); + Assert::equals('Loaded', $t->getMethod('loaded')->invoke($instance)); + Assert::equals('Not spinning', $t->getMethod('noLongerSpinning')->invoke($instance)); + } } \ No newline at end of file From 91cfd85f79cf6d6e5a45d4cb4b26980d4b2cf4e8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 10 May 2020 18:35:20 +0200 Subject: [PATCH 241/926] Refer to annotations as PHP 8 feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2caa37a4..4e3b6751 100755 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses PHP 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php Date: Mon, 11 May 2020 21:49:22 +0200 Subject: [PATCH 242/926] Remove throw expression extension Now part of xp-framework/ast builtin functionality. See #85 --- .../ast/syntax/php/ThrowExpressions.class.php | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100755 src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php diff --git a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php b/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php deleted file mode 100755 index d05d75c2..00000000 --- a/src/main/php/lang/ast/syntax/php/ThrowExpressions.class.php +++ /dev/null @@ -1,26 +0,0 @@ -prefix('throw', 0, function($parse, $token) { - return new ThrowExpression($this->expression($parse, 0), $token->line); - }); - } -} \ No newline at end of file From bd1b24469d1607b1ce5ad0c64993aac0724b2f2a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 11 May 2020 21:50:20 +0200 Subject: [PATCH 243/926] Remove lang.ast.syntax.php.ThrowExpressions from documentation Now part of xp-framework/ast builtins --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4e3b6751..a31762a4 100755 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ Usage: xp compile [] @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> lang.ast.syntax.TransformationApi lang.ast.syntax.php.NullSafe -lang.ast.syntax.php.ThrowExpressions lang.ast.syntax.php.Using @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> From 9937f864363321c5284d77cb5a91dbd0c8c349dc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 Jul 2020 18:38:32 +0200 Subject: [PATCH 244/926] Verify short circuiting works as in PHP 8 for `?->` See https://wiki.php.net/rfc/nullsafe_operator#short_circuiting --- .../ast/unittest/emit/NullSafeTest.class.php | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index 4b632eb7..1e5233bd 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -4,11 +4,11 @@ use unittest\Assert; /** - * Annotations support + * Nullsafe operator support * * @see https://github.com/xp-framework/compiler/issues/9 * @see https://docs.hhvm.com/hack/operators/null-safe - * @see https://wiki.php.net/rfc/nullsafe_calls (Draft) + * @see https://wiki.php.net/rfc/nullsafe_operator */ class NullSafeTest extends EmittingTest { @@ -101,4 +101,32 @@ public function name() { return "member"; } Assert::true($r); } + + #[@test] + public function short_circuiting_chain() { + $r= $this->run('class { + public function run() { + $null= null; + return $null?->method($undefined->method()); + } + }'); + + Assert::null($r); + } + + #[@test] + public function short_circuiting_parameter() { + $r= $this->run('class { + private function pass($object) { + return $object; + } + + public function run() { + $null= null; + return $this->pass($null?->method()); + } + }'); + + Assert::null($r); + } } \ No newline at end of file From 95708797f2c8cb4f2a95eaad3016b288b79094f4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Jul 2020 12:52:23 +0200 Subject: [PATCH 245/926] Add forward-compatible annotations emitter --- src/main/php/lang/ast/emit/PHP.class.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 980d2474..0b19ca86 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -285,13 +285,24 @@ protected function emitClass($result, $class) { } protected function emitAnnotations($result, $annotations) { - foreach ($annotations as $name => $annotation) { + foreach ($annotations as $name => $arguments) { $result->out->write("'".$name."' => "); - if ($annotation) { - $this->emitOne($result, $annotation); + + if (empty($arguments)) { + $result->out->write('null,'); + } else if ($arguments instanceof Node) { + $this->emitOne($result, $arguments); + $result->out->write(','); + } else if (1 === sizeof($arguments)) { + $this->emitOne($result, $arguments[0]); $result->out->write(','); } else { - $result->out->write('null,'); + $result->out->write('['); + foreach ($arguments as $argument) { + $this->emitOne($result, $argument); + $result->out->write(','); + } + $result->out->write('],'); } } } From b1ba3a624488491d11b549b3e4d08637761e7939 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Jul 2020 23:36:55 +0200 Subject: [PATCH 246/926] Emit null-safe instance operator `?->` --- src/main/php/lang/ast/emit/PHP.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0b19ca86..37865d7b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -765,6 +765,21 @@ protected function emitInstance($result, $instance) { } } + protected function emitNullsafeInstance($result, $instance) { + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $instance->expression); + $result->out->write(')?null:'.$t.'->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + protected function emitUnpack($result, $unpack) { $result->out->write('...'); $this->emitOne($result, $unpack->expression); From e50d2bf1a9588ad61f13b0e49998237af8a11b13 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Jul 2020 23:37:56 +0200 Subject: [PATCH 247/926] Remove `lang.ast.syntax.php.NullSafe` in favor of builtin support --- ChangeLog.md | 2 + .../lang/ast/syntax/php/NullSafe.class.php | 48 ------------------- 2 files changed, 2 insertions(+), 48 deletions(-) delete mode 100755 src/main/php/lang/ast/syntax/php/NullSafe.class.php diff --git a/ChangeLog.md b/ChangeLog.md index b54e1926..55ec95d5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Removed `lang.ast.syntax.php.NullSafe` in favor of builtin support + (@thekid) * Merged PR #84: Extract parser - @thekid ## 5.1.3 / 2020-04-04 diff --git a/src/main/php/lang/ast/syntax/php/NullSafe.class.php b/src/main/php/lang/ast/syntax/php/NullSafe.class.php deleted file mode 100755 index 08ba6c38..00000000 --- a/src/main/php/lang/ast/syntax/php/NullSafe.class.php +++ /dev/null @@ -1,48 +0,0 @@ -method(); - * - * // Rewritten to - * $value= null === ($_N0= $instance) ? null : $_N0->method() - * ``` - * - * @see https://github.com/xp-framework/compiler/issues/9 - * @test xp://lang.ast.unittest.emit.NullSafeTest - */ -class NullSafe implements Extension { - - public function setup($language, $emitter) { - $language->infix('?->', 80, function($parse, $node, $left) { - if ('{' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - $parse->expecting('}', 'dynamic member'); - } else { - $expr= new Literal($parse->token->value); - $parse->forward(); - } - - $value= new InstanceExpression($left, $expr, $node->line); - $value->kind= 'nullsafeinstance'; - return $value; - }); - - $emitter->transform('nullsafeinstance', function($codegen, $node) { - $temp= new Variable($codegen->symbol()); - $null= new Literal('null'); - return new TernaryExpression( - new BinaryExpression($null, '===', new Braced(new Assignment($temp, '=', $node->expression))), - $null, - new InstanceExpression($temp, $node->member) - ); - }); - } -} \ No newline at end of file From b6d0269899e9b15084dea46620a317d5225512d1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:38:42 +0200 Subject: [PATCH 248/926] Add support for the match expression See xp-framework/ast#8 --- src/main/php/lang/ast/emit/PHP.class.php | 28 +++++++++++ .../emit/ControlStructuresTest.class.php | 47 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 37865d7b..a3bb06cc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -545,6 +545,34 @@ protected function emitSwitch($result, $switch) { $result->out->write('}'); } + protected function emitMatch($result, $match) { + $t= $result->temp(); + $result->out->write('('.$t.'='); + $this->emitOne($result, $match->expression); + $result->out->write(')'); + + $b= 0; + foreach ($match->cases as $case) { + foreach ($case->expressions as $expression) { + $b && $result->out->write($t); + $result->out->write('==='); + $this->emitOne($result, $expression); + $result->out->write('?'); + $this->emitOne($result, $case->body[0]); + $result->out->write(':('); + $b++; + } + } + + // Emit IIFE for raising an error until we have throw expressions + if (null === $match->default) { + $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); + } else { + $this->emitOne($result, $match->default[0]); + } + $result->out->write(str_repeat(')', $b)); + } + protected function emitCatch($result, $catch) { if (empty($catch->types)) { $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 84d78680..44cfc478 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -1,5 +1,6 @@ run('class { + public function run($arg) { + $position= 1; + $size= 6100; + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + SEEK_END => min($size + $position, $size), + }; + } + }', $whence); + + Assert::equals($expected, $r); + } + + #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11], [SEEK_END, 6100]])] + public function match_with_default($whence, $expected) { + $r= $this->run('class { + public function run($arg) { + $position= 1; + $size= 6100; + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + default => min($size + $position, $size), + }; + } + }', $whence); + + Assert::equals($expected, $r); + } + + #[@test, @expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] + public function unhandled_match() { + $this->run('class { + public function run($arg) { + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + }; + } + }', SEEK_END); + } } \ No newline at end of file From 8f10b5227f425c4937613271b733aa5f9c6c75df Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:41:07 +0200 Subject: [PATCH 249/926] Fix dependency for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5c61b13a..3ceb7013 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-master as 5.0.0", + "xp-framework/ast": "dev-feature/match-expression as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From 971c6bc4807910086206f7f18e9458cfb586b0ba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:42:16 +0200 Subject: [PATCH 250/926] Revert last commit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ceb7013..5c61b13a 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-feature/match-expression as 5.0.0", + "xp-framework/ast": "dev-master as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From 51704a91c900d8b721d47d5db4c995ce5d3d30e1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:42:43 +0200 Subject: [PATCH 251/926] Fix dependency for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5c61b13a..3ceb7013 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-master as 5.0.0", + "xp-framework/ast": "dev-feature/match-expression as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From f595f5869072782c91f3b2bf76a9ea84cb256359 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:45:51 +0200 Subject: [PATCH 252/926] Simplify code inside unittests --- .../emit/ControlStructuresTest.class.php | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 44cfc478..c2f860bb 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -63,36 +63,22 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11], [SEEK_END, 6100]])] - public function match($whence, $expected) { - $r= $this->run('class { - public function run($arg) { - $position= 1; - $size= 6100; - return match ($arg) { - SEEK_SET => 10, - SEEK_CUR => $position + 10, - SEEK_END => min($size + $position, $size), - }; - } - }', $whence); - - Assert::equals($expected, $r); - } - - #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11], [SEEK_END, 6100]])] - public function match_with_default($whence, $expected) { + #[@test, @values([ + # [0, 'no items'], + # [1, 'one item'], + # [2, '2 items'], + # [3, '3 items'], + #])] + public function match($input, $expected) { $r= $this->run('class { public function run($arg) { - $position= 1; - $size= 6100; return match ($arg) { - SEEK_SET => 10, - SEEK_CUR => $position + 10, - default => min($size + $position, $size), + 0 => "no items", + 1 => "one item", + default => $arg." items", }; } - }', $whence); + }', $input); Assert::equals($expected, $r); } From 3374e722d87dbcc392d7b2de2bdb713fd4534ee2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:47:34 +0200 Subject: [PATCH 253/926] Add missing position variable --- .../php/lang/ast/unittest/emit/ControlStructuresTest.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index c2f860bb..3742329b 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -87,6 +87,7 @@ public function run($arg) { public function unhandled_match() { $this->run('class { public function run($arg) { + $position= 1; return match ($arg) { SEEK_SET => 10, SEEK_CUR => $position + 10, From 568fdde894a92f425124847d8bd362e193d4c031 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:49:15 +0200 Subject: [PATCH 254/926] Fix branch name --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3ceb7013..a7c22532 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-feature/match-expression as 5.0.0", + "xp-framework/ast": "dev-feature/match-statement as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From 186b35a11e7a70bbab68907fde6d1267e520adb3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 00:56:50 +0200 Subject: [PATCH 255/926] Verify match with binary operators --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- .../emit/ControlStructuresTest.class.php | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a3bb06cc..5e03844b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -555,9 +555,9 @@ protected function emitMatch($result, $match) { foreach ($match->cases as $case) { foreach ($case->expressions as $expression) { $b && $result->out->write($t); - $result->out->write('==='); + $result->out->write('===('); $this->emitOne($result, $expression); - $result->out->write('?'); + $result->out->write(')?'); $this->emitOne($result, $case->body[0]); $result->out->write(':('); $b++; diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 3742329b..39d1a9ad 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -83,6 +83,27 @@ public function run($arg) { Assert::equals($expected, $r); } + #[@test, @values([ + # [0, 'no items'], + # [1, 'one item'], + # [5, '5 items'], + # [10, '10+ items'], + #])] + public function match_with_binary($input, $expected) { + $r= $this->run('class { + public function run($arg) { + return match (true) { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }', $input); + + Assert::equals($expected, $r); + } + #[@test, @expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] public function unhandled_match() { $this->run('class { From 1d61efb68e13bd6f17de63af3c857d8ceb5f78ea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 01:01:10 +0200 Subject: [PATCH 256/926] Verify match works with throw expressions --- .../emit/ControlStructuresTest.class.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 39d1a9ad..22efbc47 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -96,7 +96,7 @@ public function run($arg) { $arg >= 10 => "10+ items", $arg === 1 => "one item", $arg === 0 => "no items", - default => $arg." items", + default => $arg." items", }; } }', $input); @@ -116,4 +116,18 @@ public function run($arg) { } }', SEEK_END); } + + #[@test, @expect(['class' => Throwable::class, 'withMessage' => '/Unknown seek mode .+/'])] + public function match_with_throw_expression() { + $this->run('class { + public function run($arg) { + $position= 1; + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + default => throw new \\lang\\IllegalArgumentException("Unknown seek mode ".$arg) + }; + } + }', SEEK_END); + } } \ No newline at end of file From 53dd97b12dc1d7eabda558910ba2a890efa8ca96 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 01:11:36 +0200 Subject: [PATCH 257/926] Do not enclose expressions inside match statements in array --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5e03844b..f2c775ab 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -558,7 +558,7 @@ protected function emitMatch($result, $match) { $result->out->write('===('); $this->emitOne($result, $expression); $result->out->write(')?'); - $this->emitOne($result, $case->body[0]); + $this->emitOne($result, $case->body); $result->out->write(':('); $b++; } @@ -568,7 +568,7 @@ protected function emitMatch($result, $match) { if (null === $match->default) { $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); } else { - $this->emitOne($result, $match->default[0]); + $this->emitOne($result, $match->default); } $result->out->write(str_repeat(')', $b)); } From 08bf6af8d328b1321dc91945de9c949a450a93bc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 01:18:12 +0200 Subject: [PATCH 258/926] Emit native match expression for PHP 8.0 See https://github.com/xp-framework/compiler/pull/87#discussion_r456964313 --- src/main/php/lang/ast/emit/PHP80.class.php | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index f2f74f9f..a7b5650f 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -26,4 +26,29 @@ protected function emitNew($result, $new) { $this->emitArguments($result, $new->arguments); $result->out->write(')'); } + + protected function emitMatch($result, $match) { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + + foreach ($match->cases as $case) { + $b= 0; + foreach ($case->expressions as $expression) { + $b && $result->out->write(','); + $this->emitOne($result, $expression); + $b++; + } + $result->out->write('=>'); + $this->emitOne($result, $case->body); + $result->out->write(','); + } + + if ($match->default) { + $result->out->write('default=>'); + $this->emitOne($result, $match->default); + } + + $result->out->write('}'); + } } \ No newline at end of file From 2ef515499127c86b591f56a1523b5fd15a83b231 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:31:44 +0200 Subject: [PATCH 259/926] Fix dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a7c22532..5c61b13a 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-feature/match-statement as 5.0.0", + "xp-framework/ast": "dev-master as 5.0.0", "php" : ">=7.0.0" }, "require-dev" : { From b6868355f012d37424273aba24c7b6a45c5afb3c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:32:19 +0200 Subject: [PATCH 260/926] Add support for match expression --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index 55ec95d5..739dfae5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #87: Add support for match expression - @thekid * Removed `lang.ast.syntax.php.NullSafe` in favor of builtin support (@thekid) * Merged PR #84: Extract parser - @thekid From 5f580518011d09b7407c2bc72d1be5f15d1c7669 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:38:45 +0200 Subject: [PATCH 261/926] Remove backwards compatible section of emitAnnotations() We will need to depend on xp-framework/ast v5.0.0 anyways for the added features it brings, e.g. the match expression --- src/main/php/lang/ast/emit/PHP.class.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f2c775ab..f6f0c1ce 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -290,9 +290,6 @@ protected function emitAnnotations($result, $annotations) { if (empty($arguments)) { $result->out->write('null,'); - } else if ($arguments instanceof Node) { - $this->emitOne($result, $arguments); - $result->out->write(','); } else if (1 === sizeof($arguments)) { $this->emitOne($result, $arguments[0]); $result->out->write(','); From e6f6f0bf6aa3ac0aadcd6e8b034305a4879fd1d5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:39:35 +0200 Subject: [PATCH 262/926] Remove unused emitAnnotation() --- src/main/php/lang/ast/emit/PHP.class.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f6f0c1ce..f4c2c64b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -70,10 +70,6 @@ protected function emitImport($result, $import) { } } - protected function emitAnnotation($result, $annotations) { - // NOOP - } - protected function emitOperator($result, $operator) { // NOOP } From a90f914e6b9a1e6aa616f5dd5dddd6ed65a84974 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:45:04 +0200 Subject: [PATCH 263/926] Emit "mixed" type for PHP 8 See https://wiki.php.net/rfc/mixed_type_v2 --- src/main/php/lang/ast/emit/PHP80.class.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index a7b5650f..0d1ae118 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -10,9 +10,7 @@ class PHP80 extends PHP { use RewriteBlockLambdaExpressions; - protected $unsupported= [ - 'mixed' => null, - ]; + protected $unsupported= []; protected function emitNew($result, $new) { if ($new->type instanceof Node) { From 8732ba908a86f21b25da73aaa3122a195591fb08 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:52:59 +0200 Subject: [PATCH 264/926] Link to PHP 8 constructor argument promotion RFC --- .../php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 36c571d4..6ed74446 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -9,7 +9,7 @@ * * @see https://github.com/xp-framework/rfc/issues/240 * @see https://docs.hhvm.com/hack/other-features/constructor-parameter-promotion - * @see https://wiki.php.net/rfc/constructor-promotion (Under Discussion for PHP 8.0) + * @see https://wiki.php.net/rfc/constructor_promotion (PHP 8.0) * @see https://wiki.php.net/rfc/automatic_property_initialization (Declined) */ class ArgumentPromotionTest extends EmittingTest { From 5e0dd7cf0b42b25b69ccea11baf9dc8acf2f1034 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:55:10 +0200 Subject: [PATCH 265/926] Use xp-framework/ast v5.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5c61b13a..db7cddc9 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "dev-master as 5.0.0", + "xp-framework/ast": "^5.0", "php" : ">=7.0.0" }, "require-dev" : { From 032147913b50e17c8d0660960b4f5f08c3bf6f08 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 21:56:37 +0200 Subject: [PATCH 266/926] Prepare 5.2.0-RELEASE --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 739dfae5..841b322c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.2.0 / 2020-07-20 + * Merged PR #87: Add support for match expression - @thekid +* Implemented first step of #86: Support PHP 8 attributes - @thekid * Removed `lang.ast.syntax.php.NullSafe` in favor of builtin support (@thekid) * Merged PR #84: Extract parser - @thekid From dcfe065e76ae489262c7c4ba71d3614448e5f073 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 22:01:22 +0200 Subject: [PATCH 267/926] Use php: master See https://php.watch/articles/php8-alpha1 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 602f0d76..d736cc75 100755 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,11 @@ php: - 7.2 - 7.3 - 7.4 - - nightly + - master matrix: allow_failures: - - php: nightly + - php: master before_script: - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run From e0ef1c0e37de1a068b6711108286e42826c91ce4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Jul 2020 22:05:40 +0200 Subject: [PATCH 268/926] Use php: nightly The "master" version is PHP 8.0.0-dev (cli) (built: Feb 11 2019 07:59:12) which is almost 1 1/2 years old (!), while "nightly" is at least from 2020 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d736cc75..602f0d76 100755 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,11 @@ php: - 7.2 - 7.3 - 7.4 - - master + - nightly matrix: allow_failures: - - php: master + - php: nightly before_script: - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run From 3fd00cf02117a266632166eb6561085c1715412a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 26 Jul 2020 13:41:00 +0200 Subject: [PATCH 269/926] Fix description --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db7cddc9..981444e4 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type" : "library", "homepage" : "http://xp-framework.net/", "license" : "BSD-3-Clause", - "description" : "AST for the XP Framework", + "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", From cad3c3c2f5b7b6cf320d63bd14357f5e252d06e8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Sep 2020 22:54:09 +0200 Subject: [PATCH 270/926] Forward compatible fix for comments --- src/main/php/lang/ast/emit/PHP.class.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f4c2c64b..3dbdc3ca 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -300,6 +300,16 @@ protected function emitAnnotations($result, $annotations) { } } + private function comment($comment) { + if (0 === strlen($comment)) { + return 'null'; + } else if ('/' === $comment[0]) { + return "'".str_replace("'", "\\'", trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 3, -2))))."'"; + } else { + return "'".str_replace("'", "\\'", $comment)."'"; + } + } + protected function emitMeta($result, $name, $annotations, $comment) { if (null === $name) { $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); @@ -308,7 +318,7 @@ protected function emitMeta($result, $name, $annotations, $comment) { } $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); $this->emitAnnotations($result, $annotations); - $result->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); + $result->out->write('], DETAIL_COMMENT => '.$this->comment($comment).'],'); foreach (array_shift($result->meta) as $type => $lookup) { $result->out->write($type.' => ['); @@ -322,7 +332,7 @@ protected function emitMeta($result, $name, $annotations, $comment) { $result->out->write('],'); } $result->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); - $result->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); } $result->out->write('],'); From 3a46dd6dedcf3b3960981309b954fc70449a3f49 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Sep 2020 23:00:00 +0200 Subject: [PATCH 271/926] QA: Apidocs for comment() helper --- src/main/php/lang/ast/emit/PHP.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3dbdc3ca..333320ba 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -300,6 +300,7 @@ protected function emitAnnotations($result, $annotations) { } } + /** Removes leading, intermediate and trailing stars from apidoc comments */ private function comment($comment) { if (0 === strlen($comment)) { return 'null'; From 25d03a40a22eeaf77fc8f4c46df45d2c2e78d467 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 9 Sep 2020 21:40:38 +0200 Subject: [PATCH 272/926] Prepare 5.2.1 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 841b322c..a79b10b1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.2.1 / 2020-09-09 + +* Adjusted to `xp-framework/ast` yielding comments as-is, transform + them to the form XP meta information expects. + (@thekid) + ## 5.2.0 / 2020-07-20 * Merged PR #87: Add support for match expression - @thekid From e1aec34be967635c54f25e1055a2c24652173f91 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 9 Sep 2020 21:42:20 +0200 Subject: [PATCH 273/926] Use PHP 8 annotations in example [skip ci] --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a31762a4..3a725527 100755 --- a/README.md +++ b/README.md @@ -20,16 +20,16 @@ The following code uses PHP 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 ```php > +#[Author('Timm Friebe')] class HelloWorld { public const GREETING = 'Hello'; public static function main(array $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; - $author= Type::forName(self::class)->getAnnotation('author'); + $author= Type::forName(self::class)->getAnnotation(Author::class); Console::writeLine($greet($args[0] ?? 'World', $author)); } From 326c6982e1dfbc64631b1a0d7cb26f00334fdebd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Sep 2020 12:46:32 +0200 Subject: [PATCH 274/926] Emit named arguments for PHP 8 First step for #13 - full named arguments support in PHP 7 would require knowledge about the underlying invokeable at compile time *or* deferring this to runtime - both of which require more fundamental changes --- src/main/php/lang/ast/emit/PHP.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP80.class.php | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 333320ba..7da27602 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -712,9 +712,10 @@ protected function emitInstanceOf($result, $instanceof) { protected function emitArguments($result, $arguments) { $s= sizeof($arguments) - 1; - foreach ($arguments as $i => $argument) { + $i= 0; + foreach ($arguments as $argument) { $this->emitOne($result, $argument); - if ($i < $s) $result->out->write(', '); + if ($i++ < $s) $result->out->write(', '); } } diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 0d1ae118..11ee150a 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -12,6 +12,16 @@ class PHP80 extends PHP { protected $unsupported= []; + protected function emitArguments($result, $arguments) { + $s= sizeof($arguments) - 1; + $i= 0; + foreach ($arguments as $name => $argument) { + if (is_string($name)) $result->out->write($name.':'); + $this->emitOne($result, $argument); + if ($i++ < $s) $result->out->write(', '); + } + } + protected function emitNew($result, $new) { if ($new->type instanceof Node) { $result->out->write('new ('); From fb55eea47a0ea74f645c4146c7dd5949c705f7ef Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Sep 2020 12:53:40 +0200 Subject: [PATCH 275/926] Require 5.2.0-release for xp-framework/ast library --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 981444e4..407cda3b 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "^5.0", + "xp-framework/ast": "^5.2", "php" : ">=7.0.0" }, "require-dev" : { From 8fc0972e8d84fa327e345a0c4afc682c49abbfd0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Sep 2020 15:37:58 +0200 Subject: [PATCH 276/926] Emit named arguments for PHP 8, erasing names for PHP 7 --- ChangeLog.md | 4 ++ .../unittest/emit/InvocationTest.class.php | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a79b10b1..cc313f91 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.3.0 / 2020-09-12 + +* Merged PR #88: Emit named arguments for PHP 8 - @thekid + ## 5.2.1 / 2020-09-09 * Adjusted to `xp-framework/ast` yielding comments as-is, transform diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index 5b8c5f9f..65e6c84d 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -1,6 +1,7 @@ run( + 'class { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape('.$arguments.'); + } + }' + )); + } + + #[@test, @action(new RuntimeVersion('>=8.0'))] + public function named_arguments_in_reverse_order() { + Assert::equals('html(<) = &lt;', $this->run( + 'class { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape(flags: ENT_HTML5, string: "html(<) = <"); + } + }' + )); + } + + #[@test, @action(new RuntimeVersion('>=8.0'))] + public function named_arguments_omitting_one() { + Assert::equals('html(<) = <', $this->run( + 'class { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape("html(<) = <", double: false); + } + }' + )); + } } \ No newline at end of file From c979180a7e498e67bd57bf4927246f240d572de6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Sep 2020 15:47:08 +0200 Subject: [PATCH 277/926] Use named arguments in documentation example [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a725527..df18b796 100755 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ class HelloWorld { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; $author= Type::forName(self::class)->getAnnotation(Author::class); - Console::writeLine($greet($args[0] ?? 'World', $author)); + Console::writeLine($greet($args[0] ?? 'World', from: $author)); } } ``` From e543754ec69b8fec8f0ed0edf25df297293703da Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 26 Sep 2020 23:31:40 +0200 Subject: [PATCH 278/926] Honor namespace declarations --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index cedb3e72..96c9c883 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -69,8 +69,9 @@ protected function type($code) { Console::writeLine($out->bytes()); } - $this->cl->setClassBytes($name, $out->bytes()); - return $this->cl->loadClass($name); + $class= ($package= $tree->scope()->package) ? strtr(substr($package, 1), '\\', '.').'.'.$name : $name; + $this->cl->setClassBytes($class, $out->bytes()); + return $this->cl->loadClass($class); } /** From 814b2e8980d4e8d8ffbdf7dc0db30c68123ac4a2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 27 Sep 2020 00:24:01 +0200 Subject: [PATCH 279/926] Make PHP 8 attributes backwards-compatible --- ChangeLog.md | 5 ++ src/main/php/lang/ast/emit/PHP.class.php | 9 ++-- .../unittest/emit/AnnotationsTest.class.php | 48 ++++++++++++------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index cc313f91..52c25d90 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Changed PHP 8 attributes to be emitted in XP meta information without + namespaces, and with their first characters lowercased. This way, code + using annotations will continue to work. + (@thekid) + ## 5.3.0 / 2020-09-12 * Merged PR #88: Emit named arguments for PHP 8 - @thekid diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7da27602..1c6e53ac 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -280,18 +280,21 @@ protected function emitClass($result, $class) { $result->out->write('}} '.$class->name.'::__init();'); } + /** Stores lowercased, unnamespaced name in annotations for BC reasons! */ protected function emitAnnotations($result, $annotations) { foreach ($annotations as $name => $arguments) { - $result->out->write("'".$name."' => "); + $p= strrpos($name, '\\'); + $result->out->write("'".lcfirst(false === $p ? $name : substr($name, $p + 1))."' => "); if (empty($arguments)) { $result->out->write('null,'); - } else if (1 === sizeof($arguments)) { + } else if (1 === sizeof($arguments) && isset($arguments[0])) { $this->emitOne($result, $arguments[0]); $result->out->write(','); } else { $result->out->write('['); - foreach ($arguments as $argument) { + foreach ($arguments as $name => $argument) { + is_string($name) && $result->out->write("'".$name."' => "); $this->emitOne($result, $argument); $result->out->write(','); } diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 7287ac39..69abcb46 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -8,77 +8,93 @@ * * @see https://github.com/xp-framework/rfc/issues/16 * @see https://github.com/xp-framework/rfc/issues/218 - * @see https://docs.hhvm.com/hack/attributes/introduction - * @see https://wiki.php.net/rfc/simple-annotations (Draft) - * @see https://wiki.php.net/rfc/attributes (Declined) + * @see https://wiki.php.net/rfc/shorter_attribute_syntax_change */ class AnnotationsTest extends EmittingTest { #[@test] public function without_value() { - $t= $this->type('<> class { }'); + $t= $this->type('#[Test] class { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[@test] + public function within_namespace() { + $t= $this->type('namespace tests; #[Test] class { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[@test] + public function resolved_against_import() { + $t= $this->type('use unittest\Test; #[Test] class { }'); Assert::equals(['test' => null], $t->getAnnotations()); } #[@test] public function primitive_value() { - $t= $this->type('<> class { }'); + $t= $this->type('#[Author("Timm")] class { }'); Assert::equals(['author' => 'Timm'], $t->getAnnotations()); } #[@test] public function array_value() { - $t= $this->type('<> class { }'); + $t= $this->type('#[Authors(["Timm", "Alex"])] class { }'); Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); } #[@test] public function map_value() { - $t= $this->type('< \lang\IllegalArgumentException::class])>> class { }'); + $t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class { }'); + Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + } + + #[@test] + public function named_argument() { + $t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class { }'); Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); } #[@test] public function closure_value() { - $t= $this->type('<> class { }'); + $t= $this->type('#[Verify(function($arg) { return $arg; })] class { }'); $f= $t->getAnnotation('verify'); Assert::equals('test', $f('test')); } #[@test] public function arrow_function_value() { - $t= $this->type('< $arg)>> class { }'); + $t= $this->type('#[Verify(fn($arg) => $arg)] class { }'); $f= $t->getAnnotation('verify'); Assert::equals('test', $f('test')); } #[@test] public function has_access_to_class() { - $t= $this->type('<> class { const SUCCESS = true; }'); + $t= $this->type('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }'); Assert::equals(['expect' => true], $t->getAnnotations()); } #[@test] public function method() { - $t= $this->type('class { <> public function fixture() { } }'); + $t= $this->type('class { #[Test] public function fixture() { } }'); Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); } #[@test] public function field() { - $t= $this->type('class { <> public $fixture; }'); + $t= $this->type('class { #[Test] public $fixture; }'); Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); } #[@test] public function param() { - $t= $this->type('class { public function fixture(<> $param) { } }'); + $t= $this->type('class { public function fixture(#[Test] $param) { } }'); Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); } #[@test] public function params() { - $t= $this->type('class { public function fixture(< "a"])>> $a, <> $b) { } }'); + $t= $this->type('class { public function fixture(#[inject(["name" => "a"])] $a, #[inject] $b) { } }'); $m=$t->getMethod('fixture'); Assert::equals( [['inject' => ['name' => 'a']], ['inject' => null]], @@ -88,13 +104,13 @@ public function params() { #[@test] public function multiple_class_annotations() { - $t= $this->type('<> class { }'); + $t= $this->type('#[Resource("/"), authenticated] class { }'); Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } #[@test] public function multiple_member_annotations() { - $t= $this->type('class { <> public function fixture() { } }'); + $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } From 2138303207d4c68fa5e4c071e8fdcfcb418de8e4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 27 Sep 2020 00:24:45 +0200 Subject: [PATCH 280/926] Link xp-framework/rfc#336 --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 52c25d90..d73bd2de 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,7 @@ XP Compiler ChangeLog * Changed PHP 8 attributes to be emitted in XP meta information without namespaces, and with their first characters lowercased. This way, code - using annotations will continue to work. + using annotations will continue to work, see xp-framework/rfc#336. (@thekid) ## 5.3.0 / 2020-09-12 From fe3a81b6032151f84634b7becfc81c83751d5099 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 27 Sep 2020 00:26:54 +0200 Subject: [PATCH 281/926] QA: Documentation [skip ci] --- src/main/php/lang/ast/emit/PHP.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1c6e53ac..479eee6a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -314,6 +314,7 @@ private function comment($comment) { } } + /** Emit meta information so that the reflection API won't have to parse it */ protected function emitMeta($result, $name, $annotations, $comment) { if (null === $name) { $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); From bbc250a3f40c8e486374d306ad4b4f6a675ed58f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 28 Sep 2020 19:45:46 +0200 Subject: [PATCH 282/926] Add annotation type mappings to TARGET_ANNO detail Suggested in https://github.com/xp-framework/rfc/issues/336#issuecomment-699629576 --- src/main/php/lang/ast/emit/PHP.class.php | 44 ++++++++++++++++-------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 479eee6a..2e92c14e 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -281,10 +281,13 @@ protected function emitClass($result, $class) { } /** Stores lowercased, unnamespaced name in annotations for BC reasons! */ - protected function emitAnnotations($result, $annotations) { + protected function annotations($result, $annotations) { + $lookup= []; foreach ($annotations as $name => $arguments) { $p= strrpos($name, '\\'); - $result->out->write("'".lcfirst(false === $p ? $name : substr($name, $p + 1))."' => "); + $key= lcfirst(false === $p ? $name : substr($name, $p + 1)); + $result->out->write("'".$key."' => "); + $name === $key || $lookup[$key]= $name; if (empty($arguments)) { $result->out->write('null,'); @@ -301,6 +304,25 @@ protected function emitAnnotations($result, $annotations) { $result->out->write('],'); } } + return $lookup; + } + + /** Emits annotations in XP format - and mappings for their names */ + protected function attributes($result, $annotations, $target) { + $result->out->write('DETAIL_ANNOTATIONS => ['); + $lookup= $this->annotations($result, $annotations); + $result->out->write('], DETAIL_TARGET_ANNO => ['); + foreach ($target as $name => $annotations) { + $result->out->write("'$".$name."' => ["); + foreach ($this->annotations($result, $annotations) as $key => $value) { + $lookup[$key]= $value; + } + $result->out->write('],'); + } + foreach ($lookup as $key => $value) { + $result->out->write("'".$key."' => '".$value."',"); + } + $result->out->write(']'); } /** Removes leading, intermediate and trailing stars from apidoc comments */ @@ -321,22 +343,16 @@ protected function emitMeta($result, $name, $annotations, $comment) { } else { $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); } - $result->out->write('"class" => [DETAIL_ANNOTATIONS => ['); - $this->emitAnnotations($result, $annotations); - $result->out->write('], DETAIL_COMMENT => '.$this->comment($comment).'],'); + $result->out->write('"class" => ['); + $this->attributes($result, $annotations, []); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); foreach (array_shift($result->meta) as $type => $lookup) { $result->out->write($type.' => ['); foreach ($lookup as $key => $meta) { - $result->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); - $this->emitAnnotations($result, $meta[DETAIL_ANNOTATIONS]); - $result->out->write('], DETAIL_TARGET_ANNO => ['); - foreach ($meta[DETAIL_TARGET_ANNO] as $target => $annotations) { - $result->out->write("'$".$target."' => ["); - $this->emitAnnotations($result, $annotations); - $result->out->write('],'); - } - $result->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); + $result->out->write("'".$key."' => ["); + $this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]); + $result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); } From 0f3a41f8db64dbcb4f08622004341076a2b8cc6e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 29 Sep 2020 18:35:04 +0200 Subject: [PATCH 283/926] Prepare 5.4.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d73bd2de..c32f156a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.4.0 / 2020-09-12 + +* Merged PR #89: Add annotation type mappings to `TARGET_ANNO` detail + (@thekid) * Changed PHP 8 attributes to be emitted in XP meta information without namespaces, and with their first characters lowercased. This way, code using annotations will continue to work, see xp-framework/rfc#336. From 414c563fdad90be66a77073485a1f847f83c0ebc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 29 Sep 2020 18:44:10 +0200 Subject: [PATCH 284/926] Refrain from using hacklang-style annotations <<...>> --- .../ast/unittest/emit/AnonymousClassTest.class.php | 2 +- .../php/lang/ast/unittest/emit/ParameterTest.class.php | 6 +++--- .../ast/unittest/emit/TransformationsTest.class.php | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 6bdcaa73..dc072962 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -58,7 +58,7 @@ public function method_annotations() { public function run() { return new class() { - <> + #[Inside] public function fixture() { } }; } diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index eb0b912c..6448652d 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -80,19 +80,19 @@ public function map_typed() { #[@test] public function simple_annotation() { - Assert::equals(['inject' => null], $this->param('<> $param')->getAnnotations()); + Assert::equals(['inject' => null], $this->param('#[Inject] $param')->getAnnotations()); } #[@test] public function annotation_with_value() { - Assert::equals(['inject' => 'dsn'], $this->param('<> $param')->getAnnotations()); + Assert::equals(['inject' => 'dsn'], $this->param('#[Inject("dsn")] $param')->getAnnotations()); } #[@test] public function multiple_annotations() { Assert::equals( ['inject' => null, 'name' => 'dsn'], - $this->param('<> $param')->getAnnotations() + $this->param('#[Inject, Name("dsn")] $param')->getAnnotations() ); } diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index a51a393b..998c8f50 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -10,7 +10,7 @@ class TransformationsTest extends EmittingTest { #[@before] public function setUp() { $this->transform('class', function($codegen, $class) { - if ($class->annotation('repr')) { + if ($class->annotation('Repr')) { $class->declare(new Method( ['public'], 'toString', @@ -21,7 +21,7 @@ public function setUp() { return $class; }); $this->transform('class', function($codegen, $class) { - if ($class->annotation('getters')) { + if ($class->annotation('Getters')) { foreach ($class->properties() as $property) { $class->declare(new Method( ['public'], @@ -49,7 +49,7 @@ public function __construct(int $id) { #[@test] public function generates_string_representation() { - $t= $this->type('<> class { + $t= $this->type('#[Repr] class { private int $id; public function __construct(int $id) { @@ -62,7 +62,7 @@ public function __construct(int $id) { #[@test, @values([['id', 1], ['name', 'Test']])] public function generates_accessor($name, $expected) { - $t= $this->type('<> class { + $t= $this->type('#[Getters] class { private int $id; private string $name; @@ -77,7 +77,7 @@ public function __construct(int $id, string $name) { #[@test] public function generates_both() { - $t= $this->type('<> class { + $t= $this->type('#[Repr, Getters] class { private int $id; public function __construct(int $id) { From 0e3af88f33fd8cd961655f739b18319a1095bb4a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 29 Sep 2020 18:49:46 +0200 Subject: [PATCH 285/926] Add note on hacklang annotation deprecation --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index c32f156a..e25d2d29 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 5.4.0 / 2020-09-12 +* Implemented second step for #86: Add an E_DEPRECATED warning to the + hacklang annotation syntax `<<...>>`; details in xp-framework/ast#9 + (@thekid) * Merged PR #89: Add annotation type mappings to `TARGET_ANNO` detail (@thekid) * Changed PHP 8 attributes to be emitted in XP meta information without From 020bd1609ad30e87236ee74767f09e26c3bbef0c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 1 Oct 2020 21:15:24 +0200 Subject: [PATCH 286/926] Convert annotations to PHP 8 attributes --- .../lang/ast/unittest/EmitterTest.class.php | 15 +++--- .../php/lang/ast/unittest/ScopeTest.class.php | 20 +++---- .../unittest/emit/AnnotationsTest.class.php | 54 +++++++++---------- .../emit/AnonymousClassTest.class.php | 10 ++-- .../emit/ArgumentPromotionTest.class.php | 20 +++---- .../emit/ArgumentUnpackingTest.class.php | 14 ++--- .../unittest/emit/ArrayTypesTest.class.php | 10 ++-- .../ast/unittest/emit/ArraysTest.class.php | 14 ++--- .../ast/unittest/emit/BlockTest.class.php | 6 +-- .../ast/unittest/emit/BracesTest.class.php | 22 ++++---- .../ast/unittest/emit/CastingTest.class.php | 45 +++++----------- .../ast/unittest/emit/CommentsTest.class.php | 18 +++---- .../emit/ControlStructuresTest.class.php | 36 +++---------- .../lang/ast/unittest/emit/EchoTest.class.php | 10 ++-- .../ast/unittest/emit/EmittingTest.class.php | 5 +- .../unittest/emit/ExceptionsTest.class.php | 30 +++++------ .../unittest/emit/FunctionTypesTest.class.php | 6 +-- .../lang/ast/unittest/emit/GotoTest.class.php | 6 +-- .../ast/unittest/emit/ImportTest.class.php | 10 ++-- .../unittest/emit/InstanceOfTest.class.php | 18 +++---- .../unittest/emit/InstantiationTest.class.php | 10 ++-- .../unittest/emit/InvocationTest.class.php | 27 ++++------ .../ast/unittest/emit/LambdasTest.class.php | 33 ++++++------ .../lang/ast/unittest/emit/Loading.class.php | 2 +- .../ast/unittest/emit/LoopsTest.class.php | 18 +++---- .../ast/unittest/emit/MembersTest.class.php | 32 +++++------ .../unittest/emit/MultipleCatchTest.class.php | 7 +-- .../unittest/emit/NullCoalesceTest.class.php | 9 ++-- .../ast/unittest/emit/NullSafeTest.class.php | 18 +++---- .../ast/unittest/emit/ParameterTest.class.php | 51 +++++++++--------- .../unittest/emit/PrecedenceTest.class.php | 13 ++--- .../unittest/emit/PropertyTypesTest.class.php | 8 +-- .../ast/unittest/emit/ReturnTest.class.php | 8 +-- .../ast/unittest/emit/ScalarsTest.class.php | 31 ++--------- .../lang/ast/unittest/emit/Spinner.class.php | 2 +- .../ast/unittest/emit/TernaryTest.class.php | 12 ++--- .../emit/TrailingCommasTest.class.php | 18 +++---- .../ast/unittest/emit/TraitsTest.class.php | 14 ++--- .../emit/TransformationsTest.class.php | 12 ++--- .../emit/TypeDeclarationTest.class.php | 33 +++++------- .../emit/UnicodeEscapesTest.class.php | 6 +-- .../unittest/emit/UnionTypesTest.class.php | 8 +-- .../ast/unittest/emit/UsingTest.class.php | 16 +++--- .../ast/unittest/emit/VarargsTest.class.php | 6 +-- .../ast/unittest/emit/YieldTest.class.php | 14 ++--- .../loader/CompilingClassLoaderTest.class.php | 14 ++--- 46 files changed, 349 insertions(+), 442 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 598afa90..417d21d0 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -3,8 +3,7 @@ use io\streams\MemoryOutputStream; use lang\IllegalStateException; use lang\ast\{Emitter, Node, Result}; -use unittest\Assert; -use unittest\TestCase; +use unittest\{Assert, Expect, Test, TestCase}; class EmitterTest { @@ -12,17 +11,17 @@ private function newEmitter() { return Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); } - #[@test] + #[Test] public function can_create() { $this->newEmitter(); } - #[@test] + #[Test] public function transformations_initially_empty() { Assert::equals([], $this->newEmitter()->transformations()); } - #[@test] + #[Test] public function transform() { $function= function($class) { return $class; }; @@ -31,7 +30,7 @@ public function transform() { Assert::equals(['class' => [$function]], $fixture->transformations()); } - #[@test] + #[Test] public function remove() { $first= function($class) { return $class; }; $second= function($class) { $class->annotations['author']= 'Test'; return $class; }; @@ -43,7 +42,7 @@ public function remove() { Assert::equals(['class' => [$second]], $fixture->transformations()); } - #[@test] + #[Test] public function remove_unsets_empty_kind() { $function= function($class) { return $class; }; @@ -53,7 +52,7 @@ public function remove_unsets_empty_kind() { Assert::equals([], $fixture->transformations()); } - #[@test, @expect(IllegalStateException::class)] + #[Test, Expect(IllegalStateException::class)] public function emit_node_without_kind() { $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), new class() extends Node { public $kind= null; diff --git a/src/test/php/lang/ast/unittest/ScopeTest.class.php b/src/test/php/lang/ast/unittest/ScopeTest.class.php index 1506414d..81d8aa69 100755 --- a/src/test/php/lang/ast/unittest/ScopeTest.class.php +++ b/src/test/php/lang/ast/unittest/ScopeTest.class.php @@ -1,16 +1,16 @@ package('test'); @@ -18,14 +18,14 @@ public function package() { Assert::equals('\\test', $s->package); } - #[@test] + #[Test] public function resolve_in_global_scope() { $s= new Scope(); Assert::equals('\\Parse', $s->resolve('Parse')); } - #[@test] + #[Test] public function resolve_in_package() { $s= new Scope(); $s->package('test'); @@ -33,7 +33,7 @@ public function resolve_in_package() { Assert::equals('\\test\\Parse', $s->resolve('Parse')); } - #[@test] + #[Test] public function resolve_relative_in_package() { $s= new Scope(); $s->package('test'); @@ -41,7 +41,7 @@ public function resolve_relative_in_package() { Assert::equals('\\test\\ast\\Parse', $s->resolve('ast\\Parse')); } - #[@test] + #[Test] public function resolve_imported_in_package() { $s= new Scope(); $s->package('test'); @@ -50,7 +50,7 @@ public function resolve_imported_in_package() { Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); } - #[@test] + #[Test] public function resolve_imported_in_global_scope() { $s= new Scope(); $s->import('lang\\ast\\Parse'); @@ -58,7 +58,7 @@ public function resolve_imported_in_global_scope() { Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); } - #[@test] + #[Test] public function package_inherited_from_parent() { $s= new Scope(); $s->package('test'); @@ -66,7 +66,7 @@ public function package_inherited_from_parent() { Assert::equals('\\test\\Parse', (new Scope($s))->resolve('Parse')); } - #[@test] + #[Test] public function import_inherited_from_parent() { $s= new Scope(); $s->import('lang\\ast\\Parse'); diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 69abcb46..5f4f0fba 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,7 +1,7 @@ type('#[Test] class { }'); Assert::equals(['test' => null], $t->getAnnotations()); } - #[@test] + #[Test] public function within_namespace() { $t= $this->type('namespace tests; #[Test] class { }'); Assert::equals(['test' => null], $t->getAnnotations()); } - #[@test] + #[Test] public function resolved_against_import() { $t= $this->type('use unittest\Test; #[Test] class { }'); Assert::equals(['test' => null], $t->getAnnotations()); } - #[@test] + #[Test] public function primitive_value() { $t= $this->type('#[Author("Timm")] class { }'); Assert::equals(['author' => 'Timm'], $t->getAnnotations()); } - #[@test] + #[Test] public function array_value() { $t= $this->type('#[Authors(["Timm", "Alex"])] class { }'); Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); } - #[@test] + #[Test] public function map_value() { $t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class { }'); Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); } - #[@test] + #[Test] public function named_argument() { $t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class { }'); Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); } - #[@test] + #[Test] public function closure_value() { $t= $this->type('#[Verify(function($arg) { return $arg; })] class { }'); $f= $t->getAnnotation('verify'); Assert::equals('test', $f('test')); } - #[@test] + #[Test] public function arrow_function_value() { $t= $this->type('#[Verify(fn($arg) => $arg)] class { }'); $f= $t->getAnnotation('verify'); Assert::equals('test', $f('test')); } - #[@test] + #[Test] public function has_access_to_class() { $t= $this->type('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }'); Assert::equals(['expect' => true], $t->getAnnotations()); } - #[@test] + #[Test] public function method() { $t= $this->type('class { #[Test] public function fixture() { } }'); Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); } - #[@test] + #[Test] public function field() { $t= $this->type('class { #[Test] public $fixture; }'); Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); } - #[@test] + #[Test] public function param() { $t= $this->type('class { public function fixture(#[Test] $param) { } }'); Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); } - #[@test] + #[Test] public function params() { $t= $this->type('class { public function fixture(#[inject(["name" => "a"])] $a, #[inject] $b) { } }'); $m=$t->getMethod('fixture'); @@ -102,19 +102,19 @@ public function params() { ); } - #[@test] + #[Test] public function multiple_class_annotations() { $t= $this->type('#[Resource("/"), authenticated] class { }'); Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } - #[@test] + #[Test] public function multiple_member_annotations() { $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } - #[@test] + #[Test] public function xp_type_annotation() { $t= $this->type(' #[@test] @@ -124,7 +124,7 @@ class { }' } /** @deprecated */ - #[@test] + #[Test] public function xp_type_annotation_with_named_pairs() { $t= $this->type(' #[@resource(path= "/", authenticated= true)] @@ -134,7 +134,7 @@ class { }' \xp::gc(); } - #[@test] + #[Test] public function xp_type_annotations() { $t= $this->type(' #[@resource("/"), @authenticated] @@ -143,19 +143,17 @@ class { }' Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } - #[@test] + #[Test] public function xp_type_multiline() { $t= $this->type(' - #[@verify(function($arg) { - # return $arg; - #})] + #[@verify(function($arg) {return $arg;})] class { }' ); $f= $t->getAnnotation('verify'); Assert::equals('test', $f('test')); } - #[@test] + #[Test] public function xp_method_annotations() { $t= $this->type(' class { @@ -166,9 +164,7 @@ public function succeeds() { } #[@test, @expect(\lang\IllegalArgumentException::class)] public function fails() { } - #[@test, @values([ - # [1, 2, 3], - #])] + #[@test, @values([[1, 2, 3],])] public function cases() { } }' ); @@ -177,7 +173,7 @@ public function cases() { } Assert::equals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); } - #[@test] + #[Test] public function xp_param_annotation() { $t= $this->type(' class { diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index dc072962..dd6dbd60 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -1,7 +1,7 @@ run('class { public function run() { @@ -24,7 +24,7 @@ public function id() { return "test"; } Assert::equals('test', $r->id()); } - #[@test] + #[Test] public function extending_base_class() { $r= $this->run('class { public function run() { @@ -38,7 +38,7 @@ public function initialize() { Assert::instance(AbstractDeferredInvokationHandler::class, $r); } - #[@test] + #[Test] public function implementing_interface() { $r= $this->run('class { public function run() { @@ -52,7 +52,7 @@ public function run() { Assert::instance(Runnable::class, $r); } - #[@test] + #[Test] public function method_annotations() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 6ed74446..c81a6722 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -2,7 +2,7 @@ use lang\Primitive; use lang\ast\Errors; -use unittest\Assert; +use unittest\{Assert, Expect, Test}; /** * Argument promotion @@ -14,7 +14,7 @@ */ class ArgumentPromotionTest extends EmittingTest { - #[@test] + #[Test] public function in_constructor() { $r= $this->run('class { public function __construct(private $id= "test") { @@ -28,7 +28,7 @@ public function run() { Assert::equals('test', $r); } - #[@test] + #[Test] public function can_be_used_in_constructor() { $r= $this->run('class { public function __construct(private $id= "test") { @@ -42,7 +42,7 @@ public function run() { Assert::equals('tested', $r); } - #[@test] + #[Test] public function parameter_accessible() { $r= $this->run('class { public function __construct(private $id= "test") { @@ -58,7 +58,7 @@ public function run() { Assert::equals('test', $r); } - #[@test] + #[Test] public function in_method() { $r= $this->run('class { public function withId(private $id) { @@ -72,7 +72,7 @@ public function run() { Assert::equals('test', $r); } - #[@test] + #[Test] public function type_information() { $t= $this->type('class { public function __construct(private int $id, private string $name) { @@ -84,14 +84,14 @@ public function __construct(private int $id, private string $name) { ); } - #[@test, @expect(['class' => Errors::class, 'withMessage' => 'Variadic parameters cannot be promoted'])] + #[Test, Expect(['class' => Errors::class, 'withMessage' => 'Variadic parameters cannot be promoted'])] public function variadic_parameters_cannot_be_promoted() { $this->type('class { public function __construct(private string... $in) { } }'); } - #[@test] + #[Test] public function can_be_mixed_with_normal_arguments() { $t= $this->type('class { public function __construct(public string $name, string $initial= null) { @@ -102,7 +102,7 @@ public function __construct(public string $name, string $initial= null) { Assert::equals('Timm J.', $t->newInstance('Timm', 'J')->name); } - #[@test] + #[Test] public function promoted_by_reference_argument() { $t= $this->type('class { public function __construct(public array &$list) { } @@ -118,7 +118,7 @@ public static function test() { Assert::equals([1, 2, 3, 4], $t->getMethod('test')->invoke(null, [])); } - #[@test] + #[Test] public function allows_trailing_comma() { $this->type('class { public function __construct( diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index 2e36aced..9cda4a18 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -1,7 +1,7 @@ run('class { public function fixture(... $args) { return $args; } @@ -24,7 +24,7 @@ public function run() { Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function in_array_initialization_with_variable() { $r= $this->run('class { public function run() { @@ -35,7 +35,7 @@ public function run() { Assert::equals([1, 2, 3, 4], $r); } - #[@test] + #[Test] public function in_array_initialization_with_literal() { $r= $this->run('class { public function run() { @@ -45,7 +45,7 @@ public function run() { Assert::equals([1, 2, 3, 4], $r); } - #[@test] + #[Test] public function in_map_initialization() { $r= $this->run('class { public function run() { @@ -56,7 +56,7 @@ public function run() { Assert::equals(['color' => 'red', 'type' => 'car', 'year' => 2002], $r); } - #[@test] + #[Test] public function from_generator() { $r= $this->run('class { private function items() { @@ -71,7 +71,7 @@ public function run() { Assert::equals([1, 2], $r); } - #[@test] + #[Test] public function from_iterator() { $r= $this->run('class { private function items() { diff --git a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php index 38f206cd..06814fd9 100755 --- a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php @@ -1,6 +1,6 @@ type('class { private array $test; @@ -17,7 +17,7 @@ public function int_array_type() { Assert::equals('int[]', $t->getField('test')->getType()->getName()); } - #[@test] + #[Test] public function int_map_type() { $t= $this->type('class { private array $test; @@ -26,7 +26,7 @@ public function int_map_type() { Assert::equals('[:int]', $t->getField('test')->getType()->getName()); } - #[@test] + #[Test] public function nested_map_type() { $t= $this->type('class { private array> $test; @@ -35,7 +35,7 @@ public function nested_map_type() { Assert::equals('[:int[]]', $t->getField('test')->getType()->getName()); } - #[@test] + #[Test] public function var_map_type() { $t= $this->type('class { private array $test; diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index dee00e21..fd5492e8 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -15,7 +15,7 @@ public function run() { Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function map_literal() { $r= $this->run('class { public function run() { @@ -26,7 +26,7 @@ public function run() { Assert::equals(['a' => 1, 'b' => 2], $r); } - #[@test] + #[Test] public function append() { $r= $this->run('class { public function run() { @@ -39,7 +39,7 @@ public function run() { Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function destructuring() { $r= $this->run('class { public function run() { @@ -51,7 +51,7 @@ public function run() { Assert::equals([1, 2], $r); } - #[@test] + #[Test] public function init_with_variable() { $r= $this->run('class { public function run() { @@ -63,7 +63,7 @@ public function run() { Assert::equals(['key' => 'value'], $r); } - #[@test] + #[Test] public function init_with_member_variable() { $r= $this->run('class { private static $KEY= "key"; diff --git a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php index 1ff3bdeb..37f58f37 100755 --- a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -17,7 +17,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function block_with_assignment() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index f9371ccd..930ad5aa 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -1,10 +1,10 @@ run('class { private $id= 0; @@ -17,7 +17,7 @@ public function run() { Assert::equals('test1', $r); } - #[@test] + #[Test] public function braces_around_new() { $r= $this->run('class { public function run() { @@ -28,7 +28,7 @@ public function run() { Assert::equals(250905600, $r); } - #[@test] + #[Test] public function no_braces_necessary_around_new() { $r= $this->run('class { public function run() { @@ -39,7 +39,7 @@ public function run() { Assert::equals(250905600, $r); } - #[@test] + #[Test] public function property_vs_method_ambiguity() { $r= $this->run('class { private $f; @@ -56,7 +56,7 @@ public function run() { Assert::equals('test', $r); } - #[@test] + #[Test] public function nested_braces() { $r= $this->run('class { private function test() { return "test"; } @@ -69,7 +69,7 @@ public function run() { Assert::equals('test', $r); } - #[@test] + #[Test] public function braced_expression_not_confused_with_cast() { $r= $this->run('class { const WIDTH = 640; @@ -82,11 +82,7 @@ public function run() { Assert::equals(320, $r); } - #[@test, @values(['map' => [ - # '(__LINE__)."test"' => '3test', - # '(__LINE__) + 1' => 4, - # '(__LINE__) - 1' => 2, - #]])] + #[Test, Values(['map' => ['(__LINE__)."test"' => '3test', '(__LINE__) + 1' => 4, '(__LINE__) - 1' => 2,]])] public function global_constant_in_braces_not_confused_with_cast($input, $expected) { $r= $this->run('class { public function run() { @@ -97,7 +93,7 @@ public function run() { Assert::equals($expected, $r); } - #[@test] + #[Test] public function invoke_on_braced_null_coalesce() { $r= $this->run('class { public function __invoke() { return "OK"; } diff --git a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php index 8e03bd7c..2019914e 100755 --- a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php @@ -1,18 +1,14 @@ run( 'class { @@ -24,13 +20,7 @@ public function run($value) { )); } - #[@test, @values([ - # "0", "1", "-1", "6100", - # "", - # 0.5, -1.5, - # 0, 1, -1, - # true, false - #])] + #[Test, Values(["0", "1", "-1", "6100", "", 0.5, -1.5, 0, 1, -1, true, false])] public function int_cast($value) { Assert::equals((int)$value, $this->run( 'class { @@ -42,12 +32,7 @@ public function run($value) { )); } - #[@test, @values([ - # [[]], - # [[0, 1, 2]], - # [['key' => 'value']], - # null, false, true, 1, 1.5, "", "test" - #])] + #[Test, Values([[[]], [[0, 1, 2]], [['key' => 'value']], null, false, true, 1, 1.5, "", "test"])] public function array_cast($value) { Assert::equals((array)$value, $this->run( 'class { @@ -59,7 +44,7 @@ public function run($value) { )); } - #[@test] + #[Test] public function value_cast() { Assert::equals($this, $this->run( 'class { @@ -71,7 +56,7 @@ public function run($value) { )); } - #[@test] + #[Test] public function int_array_cast() { Assert::equals([1, 2, 3], $this->run( 'class { @@ -83,7 +68,7 @@ public function run($value) { )); } - #[@test, @expect(ClassCastException::class)] + #[Test, Expect(ClassCastException::class)] public function cannot_cast_object_to_int_array() { $this->run('class { public function run() { @@ -92,7 +77,7 @@ public function run() { }'); } - #[@test, @values([null, 'test'])] + #[Test, Values([null, 'test'])] public function nullable_string_cast_of($value) { Assert::equals($value, $this->run( 'class { @@ -104,7 +89,7 @@ public function run($value) { )); } - #[@test] + #[Test] public function cast_braced() { Assert::equals(['test'], $this->run( 'class { @@ -116,7 +101,7 @@ public function run($value) { )); } - #[@test] + #[Test] public function cast_to_function_type_and_invoke() { Assert::equals($this->test(), $this->run( 'class { @@ -128,7 +113,7 @@ public function run($value) { )); } - #[@test] + #[Test] public function object_cast_on_literal() { Assert::equals((object)['key' => 'value'], $this->run( 'class { @@ -140,11 +125,7 @@ public function run($value) { )); } - #[@test, @values([ - # [1, 1], - # ['123', 123], - # [null, null] - #])] + #[Test, Values([[1, 1], ['123', 123], [null, null]])] public function nullable_int($value, $expected) { Assert::equals($expected, $this->run( 'class { @@ -156,7 +137,7 @@ public function run($value) { )); } - #[@test, @values([new Handle(10), null])] + #[Test, Values(eval: '[new Handle(10), null]')] public function nullable_value($value) { Assert::equals($value, $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php index 31c233b3..bc9b07ae 100755 --- a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php @@ -1,28 +1,28 @@ type('/** Test */ class { }'); Assert::equals('Test', $t->getComment()); } - #[@test] + #[Test] public function on_interface() { $t= $this->type('/** Test */ interface { }'); Assert::equals('Test', $t->getComment()); } - #[@test] + #[Test] public function on_trait() { $t= $this->type('/** Test */ trait { }'); Assert::equals('Test', $t->getComment()); } - #[@test] + #[Test] public function on_method() { $t= $this->type('class { @@ -35,13 +35,13 @@ public function fixture() { Assert::equals('Test', $t->getMethod('fixture')->getComment()); } - #[@test] + #[Test] public function comments_are_escaped() { $t= $this->type("/** Timm's test */ class { }"); Assert::equals("Timm's test", $t->getComment()); } - #[@test] + #[Test] public function only_last_comment_is_considered() { $t= $this->type('class { @@ -56,7 +56,7 @@ public function fixture() { Assert::equals('Test', $t->getMethod('fixture')->getComment()); } - #[@test] + #[Test] public function next_comment_is_not_considered() { $t= $this->type('class { @@ -71,7 +71,7 @@ public function fixture() { Assert::equals('Test', $t->getMethod('fixture')->getComment()); } - #[@test] + #[Test] public function inline_apidoc_comment_is_not_considered() { $t= $this->type('class { diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 22efbc47..3411f8c1 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -1,16 +1,11 @@ run('class { public function run($arg) { @@ -27,12 +22,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @values([ - # [0, 'no items'], - # [1, 'one item'], - # [2, '2 items'], - # [3, '3 items'], - #])] + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] public function switch_case($input, $expected) { $r= $this->run('class { public function run($arg) { @@ -47,7 +37,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @values([[SEEK_SET, 10], [SEEK_CUR, 11]])] + #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] public function switch_case_goto_label_ambiguity($whence, $expected) { $r= $this->run('class { public function run($arg) { @@ -63,12 +53,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @values([ - # [0, 'no items'], - # [1, 'one item'], - # [2, '2 items'], - # [3, '3 items'], - #])] + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] public function match($input, $expected) { $r= $this->run('class { public function run($arg) { @@ -83,12 +68,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @values([ - # [0, 'no items'], - # [1, 'one item'], - # [5, '5 items'], - # [10, '10+ items'], - #])] + #[Test, Values([[0, 'no items'], [1, 'one item'], [5, '5 items'], [10, '10+ items'],])] public function match_with_binary($input, $expected) { $r= $this->run('class { public function run($arg) { @@ -104,7 +84,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[@test, @expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] + #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] public function unhandled_match() { $this->run('class { public function run($arg) { @@ -117,7 +97,7 @@ public function run($arg) { }', SEEK_END); } - #[@test, @expect(['class' => Throwable::class, 'withMessage' => '/Unknown seek mode .+/'])] + #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unknown seek mode .+/'])] public function match_with_throw_expression() { $this->run('class { public function run($arg) { diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index b4740d81..a6f1d87a 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -1,6 +1,6 @@ assertEchoes('Hello', 'echo "Hello";'); } - #[@test] + #[Test] public function echo_variable() { $this->assertEchoes('Hello', '$a= "Hello"; echo $a;'); } - #[@test] + #[Test] public function echo_call() { $this->assertEchoes('Hello', 'echo $this->hello();'); } - #[@test] + #[Test] public function echo_with_multiple_arguments() { $this->assertEchoes('Hello World', 'echo "Hello", " ", "World";'); } diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 96c9c883..6c81490c 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -3,8 +3,7 @@ use io\streams\{MemoryOutputStream, StringWriter}; use lang\DynamicClassLoader; use lang\ast\{CompilingClassLoader, Emitter, Language, Result, Tokens}; -use unittest\Assert; -use unittest\TestCase; +use unittest\{After, Assert, TestCase}; use util\cmd\Console; abstract class EmittingTest { @@ -27,7 +26,7 @@ public function __construct($output= null) { } } - #[@after] + #[After] public function tearDown() { foreach ($this->transformations as $transformation) { $this->emitter->remove($transformation); diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 4ff68434..b0cd6a73 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -1,11 +1,11 @@ type('class { public function run() { @@ -20,7 +20,7 @@ public function run() { Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } - #[@test] + #[Test] public function line_number_matches() { $t= $this->type('class { public function run() { @@ -35,7 +35,7 @@ public function run() { Assert::equals(4, $t->newInstance()->run()); } - #[@test] + #[Test] public function catch_without_type() { $t= $this->type('class { public function run() { @@ -50,7 +50,7 @@ public function run() { Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } - #[@test] + #[Test] public function finally_without_exception() { $t= $this->type('class { public $closed= false; @@ -68,7 +68,7 @@ public function run() { Assert::true($instance->closed); } - #[@test] + #[Test] public function finally_with_exception() { $t= $this->type('class { public $closed= false; @@ -90,7 +90,7 @@ public function run() { } } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_null_coalesce() { $t= $this->type('class { public function run($user) { @@ -100,7 +100,7 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_short_ternary() { $t= $this->type('class { public function run($user) { @@ -110,7 +110,7 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_normal_ternary() { $t= $this->type('class { public function run($user) { @@ -120,7 +120,7 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_binary_or() { $t= $this->type('class { public function run($user) { @@ -130,7 +130,7 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_binary_and() { $t= $this->type('class { public function run($error) { @@ -140,7 +140,7 @@ public function run($error) { $t->newInstance()->run(true); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda() { $this->run('use lang\IllegalArgumentException; class { public function run() { @@ -150,7 +150,7 @@ public function run() { }'); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_throwing_variable() { $t= $this->type('class { public function run($e) { @@ -161,7 +161,7 @@ public function run($e) { $t->newInstance()->run(new IllegalArgumentException('Test')); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_variable() { $this->run('use lang\IllegalArgumentException; class { public function run() { @@ -171,7 +171,7 @@ public function run() { }'); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_parameter() { $t= $this->type('use lang\IllegalArgumentException; class { public function run($message) { diff --git a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php index 534b64cc..3f4b41c0 100755 --- a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php @@ -1,7 +1,7 @@ type('class { private (function(): string) $test; @@ -23,7 +23,7 @@ public function function_without_parameters() { ); } - #[@test] + #[Test] public function function_with_parameters() { $t= $this->type('class { private (function(int, string): string) $test; diff --git a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php index 74b259bc..202b3484 100755 --- a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -17,7 +17,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function skip_backward() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index a1552940..4525174b 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -1,7 +1,7 @@ getClassLoader()->getResourceAsStream('lang/ast/unittest/emit/import.php')->getURI()); } - #[@test] + #[Test] public function import_type() { Assert::equals(Date::class, $this->run(' use util\Date; @@ -21,7 +21,7 @@ public function run() { return Date::class; } )); } - #[@test] + #[Test] public function import_type_as_alias() { Assert::equals(Date::class, $this->run(' use util\Date as D; @@ -32,7 +32,7 @@ public function run() { return D::class; } )); } - #[@test] + #[Test] public function import_const() { Assert::equals('imported', $this->run(' use const lang\ast\unittest\emit\FIXTURE; @@ -43,7 +43,7 @@ public function run() { return FIXTURE; } )); } - #[@test] + #[Test] public function import_function() { Assert::equals('imported', $this->run(' use function lang\ast\unittest\emit\fixture; diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index 374f1b94..0a62b39a 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -15,7 +15,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function new_self_is_instanceof_this() { $r= $this->run('class { public function run() { @@ -26,7 +26,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_qualified_type() { $r= $this->run('class { public function run() { @@ -37,7 +37,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_imported_type() { $r= $this->run('use util\Date; class { public function run() { @@ -48,7 +48,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_aliased_type() { $r= $this->run('use util\Date as D; class { public function run() { @@ -59,7 +59,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_instance_expr() { $r= $this->run('class { private $type= self::class; @@ -72,7 +72,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_scope_expr() { $r= $this->run('class { private static $type= self::class; @@ -85,7 +85,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function instanceof_expr() { $r= $this->run('class { private function type() { return self::class; } diff --git a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php index ce4af54d..7c75a48f 100755 --- a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php @@ -1,11 +1,11 @@ run('class { public function run() { @@ -15,7 +15,7 @@ public function run() { Assert::instance(Date::class, $r); } - #[@test] + #[Test] public function new_var() { $r= $this->run('class { public function run() { @@ -26,7 +26,7 @@ public function run() { Assert::instance(Date::class, $r); } - #[@test] + #[Test] public function new_expr() { $r= $this->run('class { private function factory() { return \\util\\Date::class; } @@ -38,7 +38,7 @@ public function run() { Assert::instance(Date::class, $r); } - #[@test] + #[Test] public function passing_argument() { $r= $this->run('class { public $value; diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index 65e6c84d..b19e5c2f 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -1,11 +1,11 @@ run( 'class { @@ -19,7 +19,7 @@ public function run() { )); } - #[@test] + #[Test] public function instance_method_dynamic_variable() { Assert::equals('instance', $this->run( 'class { @@ -34,7 +34,7 @@ public function run() { )); } - #[@test] + #[Test] public function instance_method_dynamic_expression() { Assert::equals('instance', $this->run( 'class { @@ -49,7 +49,7 @@ public function run() { )); } - #[@test] + #[Test] public function static_method() { Assert::equals('static', $this->run( 'class { @@ -63,7 +63,7 @@ public function run() { )); } - #[@test] + #[Test] public function static_method_dynamic() { Assert::equals('static', $this->run( 'class { @@ -78,7 +78,7 @@ public function run() { )); } - #[@test] + #[Test] public function closure() { Assert::equals('closure', $this->run( 'class { @@ -91,7 +91,7 @@ public function run() { )); } - #[@test] + #[Test] public function global_function() { Assert::equals('function', $this->run( 'function fixture() { return "function"; } @@ -104,12 +104,7 @@ public function run() { )); } - #[@test, @values([ - # '"html(<) = <", flags: ENT_HTML5', - # '"html(<) = <", ENT_HTML5, double: true', - # 'string: "html(<) = <", flags: ENT_HTML5', - # 'string: "html(<) = <", flags: ENT_HTML5, double: true', - #])] + #[Test, Values(['"html(<) = <", flags: ENT_HTML5', '"html(<) = <", ENT_HTML5, double: true', 'string: "html(<) = <", flags: ENT_HTML5', 'string: "html(<) = <", flags: ENT_HTML5, double: true',])] public function named_arguments_in_exact_order($arguments) { Assert::equals('html(<) = &lt;', $this->run( 'class { @@ -125,7 +120,7 @@ public function run() { )); } - #[@test, @action(new RuntimeVersion('>=8.0'))] + #[Test, Action(eval: 'new RuntimeVersion(">=8.0")')] public function named_arguments_in_reverse_order() { Assert::equals('html(<) = &lt;', $this->run( 'class { @@ -141,7 +136,7 @@ public function run() { )); } - #[@test, @action(new RuntimeVersion('>=8.0'))] + #[Test, Action(eval: 'new RuntimeVersion(">=8.0")')] public function named_arguments_omitting_one() { Assert::equals('html(<) = <', $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 232ac285..f10fb253 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -1,8 +1,7 @@ run('class { public function run() { @@ -22,7 +21,7 @@ public function run() { Assert::equals(2, $r(1)); } - #[@test] + #[Test] public function add() { $r= $this->run('class { public function run() { @@ -33,7 +32,7 @@ public function run() { Assert::equals(3, $r(1, 2)); } - #[@test] + #[Test] public function captures_this() { $r= $this->run('class { private $addend= 2; @@ -46,7 +45,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local() { $r= $this->run('class { public function run() { @@ -58,7 +57,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_from_use_list() { $r= $this->run('class { public function run() { @@ -73,7 +72,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_from_lambda() { $r= $this->run('class { public function run() { @@ -86,7 +85,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_assigned_via_list() { $r= $this->run('class { public function run() { @@ -98,7 +97,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_param() { $r= $this->run('class { public function run($addend) { @@ -109,7 +108,7 @@ public function run($addend) { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_braced_local() { $r= $this->run('class { public function run() { @@ -121,7 +120,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function typed_parameters() { $r= $this->run('class { public function run() { @@ -132,7 +131,7 @@ public function run() { Assert::equals('lang.Value', typeof($r)->signature()[0]->getName()); } - #[@test, @action(new RuntimeVersion('>=7.0'))] + #[Test] public function typed_return() { $r= $this->run('class { public function run() { @@ -143,7 +142,7 @@ public function run() { Assert::equals('lang.Value', typeof($r)->returns()->getName()); } - #[@test] + #[Test] public function without_params() { $r= $this->run('class { public function run() { @@ -154,7 +153,7 @@ public function run() { Assert::equals(1, $r()); } - #[@test] + #[Test] public function immediately_invoked_function_expression() { $r= $this->run('class { public function run() { @@ -165,7 +164,7 @@ public function run() { Assert::equals('IIFE', $r); } - #[@test] + #[Test] public function with_block() { $r= $this->run('class { public function run() { @@ -179,7 +178,7 @@ public function run() { Assert::equals(2, $r()); } - #[@test] + #[Test] public function no_longer_supports_hacklang_variant() { try { $this->run('class { diff --git a/src/test/php/lang/ast/unittest/emit/Loading.class.php b/src/test/php/lang/ast/unittest/emit/Loading.class.php index c7f17ad9..7ee70b08 100755 --- a/src/test/php/lang/ast/unittest/emit/Loading.class.php +++ b/src/test/php/lang/ast/unittest/emit/Loading.class.php @@ -4,4 +4,4 @@ trait Loading { public function loaded() { return 'Loaded'; } -} \ No newline at end of file +} diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index c351211a..fbfd05ae 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -19,7 +19,7 @@ public function run() { Assert::equals('1,2,3', $r); } - #[@test] + #[Test] public function foreach_with_key_and_value() { $r= $this->run('class { public function run() { @@ -34,7 +34,7 @@ public function run() { Assert::equals('a=1,b=2,c=3', $r); } - #[@test] + #[Test] public function foreach_with_single_expression() { $r= $this->run('class { public function run() { @@ -47,7 +47,7 @@ public function run() { Assert::equals('1,2,3', $r); } - #[@test] + #[Test] public function for_loop() { $r= $this->run('class { public function run() { @@ -62,7 +62,7 @@ public function run() { Assert::equals('1,2,3', $r); } - #[@test] + #[Test] public function while_loop() { $r= $this->run('class { public function run() { @@ -78,7 +78,7 @@ public function run() { Assert::equals('1,2,3,4', $r); } - #[@test] + #[Test] public function do_loop() { $r= $this->run('class { public function run() { @@ -94,7 +94,7 @@ public function run() { Assert::equals('1,2,3,4', $r); } - #[@test] + #[Test] public function break_while_loop() { $r= $this->run('class { public function run() { @@ -114,7 +114,7 @@ public function run() { Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function continue_while_loop() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 260e2cef..936643a1 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,10 +1,10 @@ run('class { private static $MEMBER= "Test"; @@ -17,7 +17,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function class_method() { $r= $this->run('class { private static function member() { return "Test"; } @@ -30,7 +30,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function class_constant() { $r= $this->run('class { private const MEMBER = "Test"; @@ -43,7 +43,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function dynamic_class_property() { $r= $this->run('class { private static $MEMBER= "Test"; @@ -57,7 +57,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function dynamic_class_method() { $r= $this->run('class { private static function member() { return "Test"; } @@ -71,7 +71,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function dynamic_class_constant() { $r= $this->run('class { private const MEMBER = "Test"; @@ -85,7 +85,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function object_class_constant() { $r= $this->run('class { private const MEMBER = "Test"; @@ -98,7 +98,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test, @values(['variable', 'invocation', 'array'])] + #[Test, Values(['variable', 'invocation', 'array'])] public function class_on_objects($via) { $t= $this->type('class { private function this() { return $this; } @@ -114,7 +114,7 @@ public function array() { return [$this][0]::class; } Assert::equals(get_class($fixture), $t->getMethod($via)->invoke($fixture)); } - #[@test] + #[Test] public function instance_property() { $r= $this->run('class { private $member= "Test"; @@ -127,7 +127,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function instance_method() { $r= $this->run('class { private function member() { return "Test"; } @@ -140,7 +140,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function static_initializer_run() { $r= $this->run('class { private static $MEMBER; @@ -157,7 +157,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function enum_members() { $r= $this->run('class extends \lang\Enum { public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; @@ -170,7 +170,7 @@ public function run() { Assert::equals('MON', $r); } - #[@test] + #[Test] public function method_with_static() { $r= $this->run('class { public function run() { @@ -182,7 +182,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function method_with_static_without_initializer() { $r= $this->run('class { public function run() { @@ -194,7 +194,7 @@ public function run() { Assert::null($r); } - #[@test] + #[Test] public function chaining_sccope_operators() { $r= $this->run('class { private const TYPE = self::class; diff --git a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php index cde6d4fa..73f34529 100755 --- a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php @@ -1,7 +1,7 @@ type('class { public function run($t) { diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php index a714edfd..5a6e3787 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php @@ -1,11 +1,10 @@ =7.0'))] + #[Test] public function on_null() { $r= $this->run('class { public function run() { @@ -16,7 +15,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function on_unset_array_key() { $r= $this->run('class { public function run() { @@ -27,7 +26,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function assignment_operator() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index 1e5233bd..68ffef82 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -1,7 +1,7 @@ run('class { public function run() { @@ -24,7 +24,7 @@ public function run() { Assert::null($r); } - #[@test] + #[Test] public function method_call_on_object() { $r= $this->run('class { public function run() { @@ -38,7 +38,7 @@ public function method() { return true; } Assert::true($r); } - #[@test] + #[Test] public function member_access_on_null() { $r= $this->run('class { public function run() { @@ -50,7 +50,7 @@ public function run() { Assert::null($r); } - #[@test] + #[Test] public function member_access_on_object() { $r= $this->run('class { public function run() { @@ -64,7 +64,7 @@ public function run() { Assert::true($r); } - #[@test] + #[Test] public function chained_method_call() { $r= $this->run(' class Invocation { @@ -85,7 +85,7 @@ public function run() { Assert::equals([null, ['outer', 'inner']], $r); } - #[@test] + #[Test] public function dynamic_member_access_on_object() { $r= $this->run('class { public function run() { @@ -102,7 +102,7 @@ public function name() { return "member"; } Assert::true($r); } - #[@test] + #[Test] public function short_circuiting_chain() { $r= $this->run('class { public function run() { @@ -114,7 +114,7 @@ public function run() { Assert::null($r); } - #[@test] + #[Test] public function short_circuiting_parameter() { $r= $this->run('class { private function pass($object) { diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 6448652d..9fd5d6e4 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -1,7 +1,7 @@ param('$param')->getName()); } - #[@test] + #[Test] public function without_type() { Assert::equals(Type::$VAR, $this->param('$param')->getType()); } - #[@test, @values([ - # ['array $param', Type::$ARRAY], - # ['callable $param', Type::$CALLABLE], - # ['iterable $param', Type::$ITERABLE], - # ['object $param', Type::$OBJECT], - #])] + #[Test, Values('special')] public function with_special_type($declaration, $type) { Assert::equals($type, $this->param($declaration)->getType()); } - #[@test] + #[Test] public function value_typed() { Assert::equals(new XPClass(Value::class), $this->param('Value $param')->getType()); } - #[@test] + #[Test] public function value_type_with_null() { Assert::equals(new XPClass(Value::class), $this->param('Value $param= null')->getType()); } - #[@test] + #[Test] public function nullable_value_type() { Assert::equals(new XPClass(Value::class), $this->param('?Value $param')->getType()); } - #[@test] + #[Test] public function string_typed() { Assert::equals(Primitive::$STRING, $this->param('string $param')->getType()); } - #[@test] + #[Test] public function string_typed_with_null() { Assert::equals(Primitive::$STRING, $this->param('string $param= null')->getType()); } - #[@test] + #[Test] public function nullable_string_type() { Assert::equals(Primitive::$STRING, $this->param('?string $param')->getType()); } - #[@test] + #[Test] public function array_typed() { Assert::equals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); } - #[@test] + #[Test] public function map_typed() { Assert::equals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); } - #[@test] + #[Test] public function simple_annotation() { Assert::equals(['inject' => null], $this->param('#[Inject] $param')->getAnnotations()); } - #[@test] + #[Test] public function annotation_with_value() { Assert::equals(['inject' => 'dsn'], $this->param('#[Inject("dsn")] $param')->getAnnotations()); } - #[@test] + #[Test] public function multiple_annotations() { Assert::equals( ['inject' => null, 'name' => 'dsn'], @@ -96,22 +99,22 @@ public function multiple_annotations() { ); } - #[@test] + #[Test] public function required_parameter() { Assert::equals(false, $this->param('$param')->isOptional()); } - #[@test] + #[Test] public function optional_parameter() { Assert::equals(true, $this->param('$param= true')->isOptional()); } - #[@test] + #[Test] public function optional_parameters_default_value() { Assert::equals(true, $this->param('$param= true')->getDefaultValue()); } - #[@test] + #[Test] public function trailing_comma_allowed() { $p= $this->type('class { public function fixture($param, ) { } }') ->getMethod('fixture') diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index 4995f0af..06326114 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -1,15 +1,10 @@ run( 'class { @@ -20,7 +15,7 @@ public function run() { )); } - #[@test] + #[Test] public function concatenation() { $t= $this->type( 'class { @@ -32,7 +27,7 @@ public function run() { Assert::equals('('.$t->getName().')', $t->newInstance()->run()); } - #[@test] + #[Test] public function plusplus() { $t= $this->type( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index e6f7b38b..5f5c67eb 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -1,6 +1,6 @@ type('class { private int $test; @@ -19,7 +19,7 @@ public function int_type() { Assert::equals('int', $t->getField('test')->getTypeName()); } - #[@test] + #[Test] public function self_type() { $t= $this->type('class { private static self $instance; @@ -28,7 +28,7 @@ public function self_type() { Assert::equals('self', $t->getField('instance')->getTypeName()); } - #[@test] + #[Test] public function interface_type() { $t= $this->type('class { private \\lang\\Value $value; diff --git a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php index 351473ba..d15ddf3b 100755 --- a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -14,7 +14,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function return_member() { $r= $this->run('class { private $member= "Test"; @@ -26,7 +26,7 @@ public function run() { Assert::equals('Test', $r); } - #[@test] + #[Test] public function return_without_expression() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index 67f03207..69a089fc 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -1,46 +1,25 @@ run('class { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['135_99', 13599], - # ['107_925_284.88', 107925284.88], - # ['0xCAFE_F00D', 3405705229], - # ['0b0101_1111', 95], - # ['0137_041', 48673], - #])] + #[Test, Values([['135_99', 13599], ['107_925_284.88', 107925284.88], ['0xCAFE_F00D', 3405705229], ['0b0101_1111', 95], ['0137_041', 48673],])] public function numeric_literal_separator($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['""', ''], - # ['"Test"', 'Test'], - #])] + #[Test, Values([['""', ''], ['"Test"', 'Test'],])] public function strings($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['true', true], - # ['false', false], - # ['null', null], - #])] + #[Test, Values([['true', true], ['false', false], ['null', null],])] public function constants($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } diff --git a/src/test/php/lang/ast/unittest/emit/Spinner.class.php b/src/test/php/lang/ast/unittest/emit/Spinner.class.php index 455d2276..2cdeb64f 100755 --- a/src/test/php/lang/ast/unittest/emit/Spinner.class.php +++ b/src/test/php/lang/ast/unittest/emit/Spinner.class.php @@ -4,4 +4,4 @@ trait Spinner { public function loaded() { return 'Not spinning'; } -} \ No newline at end of file +} diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 09b7bc45..40f68ad2 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -1,11 +1,11 @@ [true => 'OK', false => 'Fail']])] + #[Test, Values(map: [true => 'OK', false => 'Fail'])] public function ternary($value, $result) { Assert::equals($result, $this->run( 'class { @@ -17,7 +17,7 @@ public function run($value) { )); } - #[@test, @values(['map' => [true => MODIFIER_PUBLIC, false => MODIFIER_PRIVATE]])] + #[Test, Values(map: [true => MODIFIER_PUBLIC, false => MODIFIER_PRIVATE])] public function ternary_constants_goto_label_ambiguity($value, $result) { Assert::equals($result, $this->run( 'class { @@ -29,7 +29,7 @@ public function run($value) { )); } - #[@test, @values(['map' => ['OK' => 'OK', null => 'Fail']])] + #[Test, Values(['map' => ['OK' => 'OK', null => 'Fail']])] public function short_ternary($value, $result) { Assert::equals($result, $this->run( 'class { @@ -41,7 +41,7 @@ public function run($value) { )); } - #[@test, @values([[['OK']], [[]]])] + #[Test, Values([[['OK']], [[]]])] public function null_coalesce($value) { Assert::equals('OK', $this->run( 'class { @@ -53,7 +53,7 @@ public function run($value) { )); } - #[@test, @values([['.'], [new Path('.')]])] + #[Test, Values(eval: '[["."], [new Path(".")]]')] public function with_instanceof($value) { Assert::equals(new Path('.'), $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php index 94fd92d0..e6bd0744 100755 --- a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php @@ -1,52 +1,52 @@ run('class { public function run() { return ["test", ]; } }'); Assert::equals(['test'], $r); } - #[@test] + #[Test] public function in_map() { $r= $this->run('class { public function run() { return ["test" => true, ]; } }'); Assert::equals(['test' => true], $r); } - #[@test] + #[Test] public function in_function_call() { $r= $this->run('class { public function run() { return sprintf("Hello %s", "test", ); } }'); Assert::equals('Hello test', $r); } - #[@test] + #[Test] public function in_parameter_list() { $r= $this->run('class { public function run($a, ) { return $a; } }', 'Test'); Assert::equals('Test', $r); } - #[@test] + #[Test] public function in_isset() { $r= $this->run('class { public function run() { return isset($a, ); } }'); Assert::equals(false, $r); } - #[@test] + #[Test] public function in_list() { $r= $this->run('class { public function run() { list($a, )= [1, 2]; return $a; } }'); Assert::equals(1, $r); } - #[@test] + #[Test] public function in_short_list() { $r= $this->run('class { public function run() { [$a, ]= [1, 2]; return $a; } }'); Assert::equals(1, $r); } - #[@test] + #[Test] public function in_namespace_group() { $r= $this->run('use lang\\{Type, }; class { public function run() { return Type::$ARRAY->getName(); } }'); Assert::equals('array', $r); diff --git a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php index be7852db..141c950a 100755 --- a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php @@ -1,7 +1,7 @@ type('class { use \lang\ast\unittest\emit\Loading; }'); Assert::equals([new XPClass(Loading::class)], $t->getTraits()); } - #[@test] + #[Test] public function trait_method_is_part_of_type() { $t= $this->type('class { use \lang\ast\unittest\emit\Loading; }'); Assert::true($t->hasMethod('loaded')); } - #[@test] + #[Test] public function trait_is_resolved() { $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading; }'); Assert::equals([new XPClass(Loading::class)], $t->getTraits()); } - #[@test] + #[Test] public function trait_method_aliased() { $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading { @@ -39,7 +39,7 @@ public function trait_method_aliased() { Assert::true($t->hasMethod('hasLoaded')); } - #[@test] + #[Test] public function trait_method_aliased_qualified() { $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading { @@ -49,7 +49,7 @@ public function trait_method_aliased_qualified() { Assert::true($t->hasMethod('hasLoaded')); } - #[@test] + #[Test] public function trait_method_insteadof() { $t= $this->type('use lang\ast\unittest\emit\{Loading, Spinner}; class { use Loading, Spinner { diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 998c8f50..1d0b5ccf 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -2,12 +2,12 @@ use lang\ast\nodes\{Method, Signature}; use lang\ast\{Code, Type}; -use unittest\Assert; +use unittest\{Assert, Before, Test, Values}; class TransformationsTest extends EmittingTest { /** @return void */ - #[@before] + #[Before] public function setUp() { $this->transform('class', function($codegen, $class) { if ($class->annotation('Repr')) { @@ -35,7 +35,7 @@ public function setUp() { }); } - #[@test] + #[Test] public function leaves_class_without_annotations() { $t= $this->type('class { private int $id; @@ -47,7 +47,7 @@ public function __construct(int $id) { Assert::false($t->hasMethod('id')); } - #[@test] + #[Test] public function generates_string_representation() { $t= $this->type('#[Repr] class { private int $id; @@ -60,7 +60,7 @@ public function __construct(int $id) { Assert::equals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); } - #[@test, @values([['id', 1], ['name', 'Test']])] + #[Test, Values([['id', 1], ['name', 'Test']])] public function generates_accessor($name, $expected) { $t= $this->type('#[Getters] class { private int $id; @@ -75,7 +75,7 @@ public function __construct(int $id, string $name) { Assert::equals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); } - #[@test] + #[Test] public function generates_both() { $t= $this->type('#[Repr, Getters] class { private int $id; diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index e9ceab1a..d23f945e 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -2,11 +2,11 @@ use lang\XPClass; use lang\reflect\Modifiers; -use unittest\Assert; +use unittest\{Assert, Test, Values}; class TypeDeclarationTest extends EmittingTest { - #[@test, @values(['class', 'interface', 'trait'])] + #[Test, Values(['class', 'interface', 'trait'])] public function empty_type($kind) { $t= $this->type($kind.' { }'); Assert::equals( @@ -15,22 +15,22 @@ public function empty_type($kind) { ); } - #[@test] + #[Test] public function abstract_class_type() { Assert::true(Modifiers::isAbstract($this->type('abstract class { }')->getModifiers())); } - #[@test] + #[Test] public function final_class_type() { Assert::true(Modifiers::isFinal($this->type('final class { }')->getModifiers())); } - #[@test] + #[Test] public function class_without_parent() { Assert::null($this->type('class { }')->getParentclass()); } - #[@test] + #[Test] public function class_with_parent() { Assert::equals( new XPClass(EmittingTest::class), @@ -38,26 +38,23 @@ public function class_with_parent() { ); } - #[@test] + #[Test] public function trait_type() { Assert::true($this->type('trait { }')->isTrait()); } - #[@test] + #[Test] public function interface_type() { Assert::true($this->type('interface { }')->isInterface()); } - #[@test, @values(['public', 'private', 'protected'])] + #[Test, Values(['public', 'private', 'protected'])] public function constant($modifiers) { $c= $this->type('class { '.$modifiers.' const test = 1; }')->getConstant('test'); Assert::equals(1, $c); } - #[@test, @values([ - # 'public', 'private', 'protected', - # 'public static', 'private static', 'protected static' - #])] + #[Test, Values(['public', 'private', 'protected', 'public static', 'private static', 'protected static'])] public function field($modifiers) { $f= $this->type('class { '.$modifiers.' $test; }')->getField('test'); $n= implode(' ', Modifiers::namesOf($f->getModifiers())); @@ -67,11 +64,7 @@ public function field($modifiers) { ); } - #[@test, @values([ - # 'public', 'protected', 'private', - # 'public final', 'protected final', - # 'public static', 'protected static', 'private static' - #])] + #[Test, Values(['public', 'protected', 'private', 'public final', 'protected final', 'public static', 'protected static', 'private static'])] public function method($modifiers) { $m= $this->type('class { '.$modifiers.' function test() { } }')->getMethod('test'); $n= implode(' ', Modifiers::namesOf($m->getModifiers())); @@ -81,13 +74,13 @@ public function method($modifiers) { ); } - #[@test] + #[Test] public function abstract_method() { $m= $this->type('abstract class { abstract function test(); }')->getMethod('test'); Assert::true(Modifiers::isAbstract($m->getModifiers())); } - #[@test] + #[Test] public function method_with_keyword() { $t= $this->type('class { private $items; diff --git a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php index 50b29757..d0c21758 100755 --- a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -15,7 +15,7 @@ public function run() { Assert::equals('mañana', $r); } - #[@test] + #[Test] public function emoji() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index b2ac363f..b187a6b9 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -1,7 +1,7 @@ type('class { private int|string $test; @@ -22,7 +22,7 @@ public function field_type() { ); } - #[@test] + #[Test] public function parameter_type() { $t= $this->type('class { public function test(int|string $arg) { } @@ -34,7 +34,7 @@ public function test(int|string $arg) { } ); } - #[@test] + #[Test] public function return_type() { $t= $this->type('class { public function test(): int|string { } diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index b568f2cb..47700ca7 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -1,6 +1,6 @@ run('use lang\ast\unittest\emit\Handle; class { public function run() { @@ -24,7 +24,7 @@ public function run() { Assert::equals(['read@1', '__dispose@1'], $r); } - #[@test] + #[Test] public function dispose_called_for_all() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { public function run() { @@ -40,7 +40,7 @@ public function run() { Assert::equals(['read@1', '__dispose@1', '__dispose@2'], $r); } - #[@test] + #[Test] public function dispose_called_even_when_exceptions_occur() { $r= $this->run('use lang\{IllegalArgumentException, IllegalStateException}; use lang\ast\unittest\emit\Handle; class { public function run() { @@ -60,7 +60,7 @@ public function run() { Assert::equals(['read@1', '__dispose@1'], $r); } - #[@test] + #[Test] public function supports_closeables() { $r= $this->run('use lang\ast\unittest\emit\FileInput; class { public function run() { @@ -76,7 +76,7 @@ public function run() { Assert::false($r); } - #[@test] + #[Test] public function can_return_from_inside_using() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { private function read() { @@ -94,7 +94,7 @@ public function run() { Assert::equals(['called' => ['read@1', '__dispose@1'], 'returned' => 'test'], $r); } - #[@test] + #[Test] public function variable_undefined_after_using() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { public function run() { @@ -107,7 +107,7 @@ public function run() { Assert::false($r); } - #[@test] + #[Test] public function variable_undefined_after_using_even_if_previously_defined() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index 6421fd3a..83feb0c8 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -1,10 +1,10 @@ run('class { private function format(string $format, ... $args) { @@ -19,7 +19,7 @@ public function run() { Assert::equals('Hello Test', $r); } - #[@test] + #[Test] public function list_of() { $r= $this->run('class { private function listOf(string... $args) { diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index 00e64fa3..2b619a02 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -1,10 +1,10 @@ run('class { public function run() { @@ -15,7 +15,7 @@ public function run() { Assert::equals([null, null], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_values() { $r= $this->run('class { public function run() { @@ -27,7 +27,7 @@ public function run() { Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_keys_and_values() { $r= $this->run('class { public function run() { @@ -38,7 +38,7 @@ public function run() { Assert::equals(['color' => 'orange', 'price' => 12.99], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_array() { $r= $this->run('class { public function run() { @@ -48,7 +48,7 @@ public function run() { Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_generator() { $r= $this->run('class { private function values() { @@ -64,7 +64,7 @@ public function run() { Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_and_yield() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index be5399d2..ae00031a 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -3,8 +3,7 @@ use io\{File, FileUtil, Folder}; use lang\ast\CompilingClassLoader; use lang\{ClassFormatException, ClassLoader, Environment}; -use unittest\Assert; -use unittest\TestCase; +use unittest\{Assert, Expect, Test, TestCase}; class CompilingClassLoaderTest { private static $runtime; @@ -37,25 +36,22 @@ private function load($type, $source) { } } - #[@test] + #[Test] public function can_create() { CompilingClassLoader::instanceFor(self::$runtime); } - #[@test] + #[Test] public function load_class() { Assert::equals('Tests', $this->load('Tests', 'getSimpleName()); } - #[@test, @expect([ - # 'class' => ClassFormatException::class, - # 'withMessage' => 'Compiler error: Expected "{", have "(end)"' - #])] + #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => 'Compiler error: Expected "{", have "(end)"'])] public function load_class_with_syntax_errors() { $this->load('Errors', "load('Triggers', ' Date: Fri, 9 Oct 2020 21:30:46 +0200 Subject: [PATCH 287/926] Make compiler compatible with AST library 6.0.0 --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 407cda3b..37860694 100755 --- a/composer.json +++ b/composer.json @@ -7,8 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/tokenize": "^9.0 | ^8.1", - "xp-framework/ast": "^5.2", + "xp-framework/ast": "^6.0 | ^5.2", "php" : ">=7.0.0" }, "require-dev" : { From f5ade4964b3742ff3d1c3cec8e69da7c12c9df0a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Oct 2020 21:31:26 +0200 Subject: [PATCH 288/926] Write preamble inside Result class instead of emitter See https://github.com/xp-framework/compiler/issues/90#issuecomment-706364456 --- src/main/php/lang/ast/Result.class.php | 10 ++++++++-- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- .../php/lang/ast/unittest/emit/EmittingTest.class.php | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index f3e46b04..3c18e12a 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -11,9 +11,15 @@ class Result { public $stack= []; public $call= []; - /** @param io.streams.Writer */ - public function __construct($out) { + /** + * Starts an result stream, including a preamble + * + * @param io.streams.Writer + * @param string $preamble + */ + public function __construct($out, $preamble= 'out= $out; + $this->out->write($preamble); $this->codegen= new CodeGen(); } diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 2e92c14e..73c1445e 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -57,11 +57,11 @@ protected function propertyType($type) { } protected function emitStart($result, $start) { - $result->out->write('out->write('namespace '.$declaration->name."\n"); + $result->out->write('namespace '.$declaration->name); } protected function emitImport($result, $import) { diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 6c81490c..068a34c8 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -61,7 +61,7 @@ protected function type($code) { Console::writeLine($tree); } - $this->emitter->emitAll(new Result(new StringWriter($out)), $tree->children()); + $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); From 3b6dae6093011db00247d0756322c82f5c5f0bcd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Oct 2020 21:38:05 +0200 Subject: [PATCH 289/926] Use php "master" - "nightly" refers to a PHP build from January 2020 (!) --- .travis.yml | 4 ++-- ChangeLog.md | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 602f0d76..d736cc75 100755 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,11 @@ php: - 7.2 - 7.3 - 7.4 - - nightly + - master matrix: allow_failures: - - php: nightly + - php: master before_script: - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run diff --git a/ChangeLog.md b/ChangeLog.md index e25d2d29..aa402961 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.4.1 / 2020-10-09 + +* Fixed #90: Namespace declaration statement has to be the very first + statement, which occured with PHP 8.0.0RC1 + (@thekid) + ## 5.4.0 / 2020-09-12 * Implemented second step for #86: Add an E_DEPRECATED warning to the From 9bcb1b349a30e6e090d3a1265b9e284adcca18c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Oct 2020 21:40:55 +0200 Subject: [PATCH 290/926] Remove "dist: trusty" --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d736cc75..adc7e6f3 100755 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: php -dist: trusty - php: - 7.0 - 7.1 From 4963c86ae2b68b68ba74020df92364e99fb3a98c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Oct 2020 22:00:15 +0200 Subject: [PATCH 291/926] Fix example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df18b796..dd5eedbc 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ class HelloWorld { public static function main(array $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; - $author= Type::forName(self::class)->getAnnotation(Author::class); + $author= Type::forName(self::class)->getAnnotation('author'); Console::writeLine($greet($args[0] ?? 'World', from: $author)); } From 2f884ee2ad9ee4ab1d6cd6435bfa11e05ef3e09b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:44:00 +0100 Subject: [PATCH 292/926] Emit union types as syntax in PHP 8 See https://wiki.php.net/rfc/union_types_v2 --- ChangeLog.md | 2 ++ src/main/php/lang/ast/emit/PHP80.class.php | 29 +++++++++++++++- .../unittest/emit/UnionTypesTest.class.php | 33 ++++++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index aa402961..6f608c68 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Emit union types as syntax in PHP 8 - @thekid + ## 5.4.1 / 2020-10-09 * Fixed #90: Namespace declaration statement has to be the very first diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 11ee150a..7c551465 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,6 +1,7 @@ components as $component) { + $literal.= '|'.$this->paramType($component); + } + return substr($literal, 1); + } else { + return $type->literal(); + } + } + + protected function paramType($type) { return $this->literal($type); } + + protected function returnType($type) { return $this->literal($type); } protected function emitArguments($result, $arguments) { $s= sizeof($arguments) - 1; diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index b187a6b9..bdcf3bcc 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -1,12 +1,13 @@ getMethod('test')->getReturnType() ); } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + public function parameter_type_restriction_with_php8() { + $t= $this->type('class { + public function test(int|string|array $arg) { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), + $t->getMethod('test')->getParameter(0)->getTypeRestriction() + ); + } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + public function return_type_restriction_with_php8() { + $t= $this->type('class { + public function test(): int|string|array { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), + $t->getMethod('test')->getReturnTypeRestriction() + ); + } } \ No newline at end of file From 35e4a543b41668a4d2949c30dd7a2eca4e9a34b9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:47:42 +0100 Subject: [PATCH 293/926] QA: Simplify code in propertyType() --- src/main/php/lang/ast/emit/PHP.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 73c1445e..5c8593c9 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -49,10 +49,10 @@ protected function propertyType($type) { return ''; } else if ($type instanceof IsArray || $type instanceof IsMap) { return 'array'; - } else if ($type instanceof Type && 'callable' !== $type->literal() && 'void' !== $type->literal()) { - return $type->literal(); - } else { + } else if ('callable' === $type->literal() || 'void' === $type->literal()) { return ''; + } else { + return $type->literal(); } } From e6bd180e8fc4af7cd382a614b95415a37c23abd2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:50:49 +0100 Subject: [PATCH 294/926] Save one level of indirection --- src/main/php/lang/ast/emit/PHP80.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 7c551465..e5a0211a 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -27,7 +27,7 @@ protected function literal($type) { } else if ($type instanceof IsUnion) { $literal= ''; foreach ($type->components as $component) { - $literal.= '|'.$this->paramType($component); + $literal.= '|'.$this->literal($component); } return substr($literal, 1); } else { From 578b95f3ca065b4499219f7ff4a6a6ec03f6f7be Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:53:59 +0100 Subject: [PATCH 295/926] Verify arrays and map types are emitted as Type::$ARRAY --- ChangeLog.md | 2 ++ .../php/lang/ast/unittest/emit/ParameterTest.class.php | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6f608c68..a687b1e3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.5.0 / 2020-11-14 + * Emit union types as syntax in PHP 8 - @thekid ## 5.4.1 / 2020-10-09 diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 9fd5d6e4..44a8d402 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -76,11 +76,21 @@ public function array_typed() { Assert::equals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); } + #[Test] + public function array_typed_restriction() { + Assert::equals(Type::$ARRAY, $this->param('array $param')->getTypeRestriction()); + } + #[Test] public function map_typed() { Assert::equals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); } + #[Test] + public function map_typed_restriction() { + Assert::equals(Type::$ARRAY, $this->param('array $param')->getTypeRestriction()); + } + #[Test] public function simple_annotation() { Assert::equals(['inject' => null], $this->param('#[Inject] $param')->getAnnotations()); From c6d2f7b3ba66e106dc48ac7b1dd96fbcd3e18fa1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:57:53 +0100 Subject: [PATCH 296/926] Add GitHub actions --- .github/workflows/ci.yml | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 00000000..a8d011c7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +.github/workflows/ci.yml + +name: Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + tests: + name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.php-versions == '8.0' }} + strategy: + fail-fast: false + matrix: + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] + os: [ubuntu-latest, windows-latest] + + steps: + - name: Configure git + if: runner.os == 'Windows' + run: git config --system core.autocrlf false; git config --system core.eol lf + + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up PHP ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + ini-values: date.timezone=Europe/Berlin + + - name: Setup Problem Matchers for PHP + run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2.1.3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: + - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run + - composer install --prefer-dist + - echo "vendor/autoload.php" > composer.pth + + - name: Run test suite + run: sh xp-run xp.unittest.TestRunner src/test/php From 463081678ade6ae0e0120bf14fc55d49f03a31b0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:58:18 +0100 Subject: [PATCH 297/926] Fix typo --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8d011c7..7fc7a2f2 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,3 @@ -.github/workflows/ci.yml - name: Tests on: From d0e4d24ffbec50cf21debf424b4e88aee2fa5e8e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 12:59:47 +0100 Subject: [PATCH 298/926] Use multiline string --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fc7a2f2..64052d07 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,10 +49,10 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install dependencies - run: - - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run - - composer install --prefer-dist - - echo "vendor/autoload.php" > composer.pth + run: > + curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run + composer install --prefer-dist + echo "vendor/autoload.php" > composer.pth - name: Run test suite run: sh xp-run xp.unittest.TestRunner src/test/php From 83df8fe3c3c1ff5f00e01852ad89797ed83fa675 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 13:01:07 +0100 Subject: [PATCH 299/926] Use && to concatenate commands --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64052d07..95950b9d 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,8 +50,8 @@ jobs: - name: Install dependencies run: > - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run - composer install --prefer-dist + curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run && + composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth - name: Run test suite From 64b4f6f66de18e283fb11eb8e5c2fc3251dba3c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 Nov 2020 13:03:34 +0100 Subject: [PATCH 300/926] Add GitHub tests badge [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd5eedbc..30ea88dc 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ XP Compiler =========== +[![Build status on GitHub](https://github.com/xp-framework/compiler/workflows/Tests/badge.svg)](https://github.com/xp-framework/compiler/actions) [![Build Status on TravisCI](https://secure.travis-ci.org/xp-forge/sequence.svg)](http://travis-ci.org/xp-framework/compiler) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) From 1395e56190e2cc2d669cd4b8cba9f319a38aa063 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 12:23:45 +0100 Subject: [PATCH 301/926] Refactor rewriting type literals Create a visitor-like pattern using a map of type classes to emitter functions, held together in lang.ast.emit.PHP::literal() --- src/main/php/lang/ast/emit/PHP.class.php | 28 ++++-------- src/main/php/lang/ast/emit/PHP70.class.php | 23 +++++++--- src/main/php/lang/ast/emit/PHP71.class.php | 21 +++++++-- src/main/php/lang/ast/emit/PHP72.class.php | 20 +++++++-- src/main/php/lang/ast/emit/PHP74.class.php | 20 +++++++-- src/main/php/lang/ast/emit/PHP80.class.php | 45 ++++++++----------- .../ast/unittest/emit/ParameterTest.class.php | 8 +++- .../emit/TransformationsTest.class.php | 5 ++- 8 files changed, 104 insertions(+), 66 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5c8593c9..dfbfaff5 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ unsupported[$name]) - ) ? null : $name; - } - - protected function paramType($type) { - return $this->type($type->literal()); - } - - protected function returnType($type) { - return $this->type($type->literal()); + protected function literal($type) { + return null === $type ? null : $this->literals[get_class($type)]($type); } // See https://wiki.php.net/rfc/typed_properties_v2#supported_types @@ -187,7 +175,7 @@ protected function emitArray($result, $array) { } protected function emitParameter($result, $parameter) { - if ($parameter->type && $t= $this->paramType($parameter->type)) { + if ($parameter->type && $t= $this->literal($parameter->type)) { $result->out->write($t.' '); } if ($parameter->variadic) { @@ -211,7 +199,7 @@ protected function emitSignature($result, $signature) { } $result->out->write(')'); - if ($signature->returns && $t= $this->returnType($signature->returns)) { + if ($signature->returns && $t= $this->literal($signature->returns)) { $result->out->write(':'.$t); } } diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index e29bd7f4..81a23563 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,5 +1,7 @@ 72, - 'void' => 71, - 'iterable' => 71, - 'mixed' => null, - ]; + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsUnion::class => function($t) { return null; }, + IsFunction::class => function($t) { return null; }, + IsNullable::class => function($t) { return null; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsLiteral::class => function($t) { + $l= $t->literal(); + return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l) ? null : $l; + }, + ]; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 14c039a1..6c96e79c 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -1,5 +1,7 @@ 72, - 'mixed' => null, - ]; + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsUnion::class => function($t) { return null; }, + IsFunction::class => function($t) { return null; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsLiteral::class => function($t) { + $l= $t->literal(); + return ('object' === $l || 'mixed' === $l) ? null : $l; + }, + ]; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index bfe5593d..3abd7dab 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -1,5 +1,7 @@ null, - ]; + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsUnion::class => function($t) { return null; }, + IsFunction::class => function($t) { return null; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsLiteral::class => function($t) { + $l= $t->literal(); + return 'mixed' === $l ? null : $l; + }, + ]; + } } \ 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 d31030ec..b147c200 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -1,5 +1,7 @@ null, - ]; + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsUnion::class => function($t) { return null; }, + IsFunction::class => function($t) { return null; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsLiteral::class => function($t) { + $l= $t->literal(); + return 'mixed' === $l ? null : $l; + }, + ]; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index e5a0211a..8d64f8cc 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,7 +1,7 @@ components as $component) { - $literal.= '|'.$this->literal($component); - } - return substr($literal, 1); - } else { - return $type->literal(); - } + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsFunction::class => function($t) { return null; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { + $l= ''; + foreach ($t->components as $component) { + $l.= '|'.$this->literal($component); + } + return substr($l, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); } + ]; } - protected function paramType($type) { return $this->literal($type); } - - protected function returnType($type) { return $this->literal($type); } - protected function emitArguments($result, $arguments) { $s= sizeof($arguments) - 1; $i= 0; diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 44a8d402..346ace88 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -1,7 +1,8 @@ param('?string $param')->getType()); } + #[Test, Action(eval: 'new RuntimeVersion(">=7.1")')] + public function nullable_string_type_restriction() { + Assert::equals(Primitive::$STRING, $this->param('?string $param')->getTypeRestriction()); + } + #[Test] public function array_typed() { Assert::equals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 1d0b5ccf..ea170a42 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -1,7 +1,8 @@ declare(new Method( ['public'], 'toString', - new Signature([], new Type('string')), + new Signature([], new IsLiteral('string')), [new Code('return "T@".\util\Objects::stringOf(get_object_vars($this))')] )); } From 63efe5e14af8595b0033964183dc6cb213dcbb80 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 12:59:53 +0100 Subject: [PATCH 302/926] Emit function types as `callable` in all PHP versions --- src/main/php/lang/ast/emit/PHP70.class.php | 6 +++--- src/main/php/lang/ast/emit/PHP71.class.php | 4 ++-- src/main/php/lang/ast/emit/PHP72.class.php | 4 ++-- src/main/php/lang/ast/emit/PHP74.class.php | 6 +++--- src/main/php/lang/ast/emit/PHP80.class.php | 9 +++++---- .../lang/ast/unittest/emit/UnionTypesTest.class.php | 12 ++++++++++++ 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 81a23563..49607f4d 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -14,12 +14,12 @@ class PHP70 extends PHP { /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ - IsUnion::class => function($t) { return null; }, - IsFunction::class => function($t) { return null; }, - IsNullable::class => function($t) { return null; }, + IsFunction::class => function($t) { return 'callable'; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { return null; }, + IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l) ? null : $l; diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 6c96e79c..3f2c025c 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -14,12 +14,12 @@ class PHP71 extends PHP { /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ - IsUnion::class => function($t) { return null; }, - IsFunction::class => function($t) { return null; }, + IsFunction::class => function($t) { return 'callable'; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, IsValue::class => function($t) { return $t->literal(); }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); return ('object' === $l || 'mixed' === $l) ? null : $l; diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 3abd7dab..93a6c431 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -14,12 +14,12 @@ class PHP72 extends PHP { /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ - IsUnion::class => function($t) { return null; }, - IsFunction::class => function($t) { return null; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); return 'mixed' === $l ? null : $l; diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index b147c200..50e4b433 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -1,6 +1,6 @@ literal mappings */ public function __construct() { $this->literals= [ - IsUnion::class => function($t) { return null; }, - IsFunction::class => function($t) { return null; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); return 'mixed' === $l ? null : $l; diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 8d64f8cc..dc14f9bc 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -14,17 +14,18 @@ class PHP80 extends PHP { /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ - IsFunction::class => function($t) { return null; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, IsUnion::class => function($t) { - $l= ''; + $u= ''; foreach ($t->components as $component) { - $l.= '|'.$this->literal($component); + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; } - return substr($l, 1); + return substr($u, 1); }, IsLiteral::class => function($t) { return $t->literal(); } ]; diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index bdcf3bcc..96d16476 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -59,6 +59,18 @@ public function test(int|string|array $arg) { } ); } + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + public function parameter_function_type_restriction_with_php8() { + $t= $this->type('class { + public function test(): string|(function(): string) { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$STRING, Type::$CALLABLE]), + $t->getMethod('test')->getReturnTypeRestriction() + ); + } + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] public function return_type_restriction_with_php8() { $t= $this->type('class { From 7f1d3e3ea4ccc2cf044c10637be78d558b9fcbae Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 12:59:58 +0100 Subject: [PATCH 303/926] QA: Formatting --- ChangeLog.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index a687b1e3..b9521249 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,9 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 5.5.0 / 2020-11-14 +## 5.5.0 / 2020-11-15 -* Emit union types as syntax in PHP 8 - @thekid +* Changed types emitter implementation: + - Emit function types as `callable` in all PHP versions + - Emit union types as syntax in PHP 8+ + (@thekid) ## 5.4.1 / 2020-10-09 From bd93b14651d8026657d362b4fce796a0000ec059 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:02:03 +0100 Subject: [PATCH 304/926] QA: Remove unused import --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index dfbfaff5..9740ad31 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ Date: Sun, 15 Nov 2020 13:04:23 +0100 Subject: [PATCH 305/926] Reference pull request in changelog --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index b9521249..88069c9a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,8 @@ XP Compiler ChangeLog ## 5.5.0 / 2020-11-15 -* Changed types emitter implementation: +* Merged PR #91 - Refactor rewriting type literals: + - Changed implementation to be easier to maintain - Emit function types as `callable` in all PHP versions - Emit union types as syntax in PHP 8+ (@thekid) From 934f8668e9c996933a8433028b6bd00dbc015915 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:07:04 +0100 Subject: [PATCH 306/926] QA: Remove unused instance variable --- src/main/php/lang/ast/emit/PHP.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9740ad31..3bf5f02d 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -8,7 +8,6 @@ abstract class PHP extends Emitter { const PROPERTY = 0; const METHOD = 1; - protected $unsupported= []; protected $literals= []; /** From f0a768da9f7e35644269ee657f248f7504c01f84 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:07:25 +0100 Subject: [PATCH 307/926] QA: Fix type in API doc --- src/main/php/lang/ast/emit/PHP.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3bf5f02d..67811bbb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -21,10 +21,10 @@ protected function declaration($name) { } /** - * Emit type literal + * Emit type literal or NULL if no type should be emitted * - * @param ?lang.ast.types.Type $type - * @return string + * @param ?lang.ast.Type $type + * @return ?string */ protected function literal($type) { return null === $type ? null : $this->literals[get_class($type)]($type); From 2f231913d218b2025a29a01585be2f51e65c255a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:11:45 +0100 Subject: [PATCH 308/926] Simplify propertyType() method --- src/main/php/lang/ast/emit/PHP.class.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 67811bbb..0538c4fb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -30,16 +30,19 @@ protected function literal($type) { return null === $type ? null : $this->literals[get_class($type)]($type); } - // See https://wiki.php.net/rfc/typed_properties_v2#supported_types + /** + * As of PHP 7.4: Property type declarations support all type declarations + * supported by PHP with the exception of void and callable. + * + * @see https://wiki.php.net/rfc/typed_properties_v2#supported_types + * @param ?lang.ast.Type $type + * @return ?string + */ protected function propertyType($type) { - if (null === $type || $type instanceof IsUnion || $type instanceof IsFunction) { - return ''; - } else if ($type instanceof IsArray || $type instanceof IsMap) { - return 'array'; - } else if ('callable' === $type->literal() || 'void' === $type->literal()) { - return ''; + if (null === $type || $type instanceof IsFunction || 'callable' === $type->literal()) { + return null; } else { - return $type->literal(); + return $this->literal($type); } } From 4b4a4dc7cefc50649e6da29789b486461b3f28bb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:30:38 +0100 Subject: [PATCH 309/926] QA: Adjust `xp compile` output to reality --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30ea88dc..3b0ed5b3 100755 --- a/README.md +++ b/README.md @@ -51,9 +51,10 @@ $ composer require xp-lang/php-is-operator $ xp compile Usage: xp compile [] -@FileSystemCL<./vendor/xp-framework/compiler/src/main/php> +@FileSystemCL<./vendor/xp-framework/ast/src/main/php lang.ast.syntax.TransformationApi -lang.ast.syntax.php.NullSafe + +@FileSystemCL<./vendor/xp-framework/compiler/src/main/php> lang.ast.syntax.php.Using @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> From 3476cfcfd81e46a23239503db3515f0676aa4307 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 13:49:41 +0100 Subject: [PATCH 310/926] Add note on compilation during class loading and manual compilation --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 3b0ed5b3..dc6974e5 100755 --- a/README.md +++ b/README.md @@ -37,6 +37,29 @@ class HelloWorld { } ``` +Compilation +----------- +By default, XP Compiler will hook into the class loading chain and compile files on-demand. This keeps the code-save-reload/rerun development process typical for PHP. However, compilation can also be performed manually by invoking the compiler: + +```bash +# Compile code and write result to a class file +$ xp compile HelloWorld.php HelloWorld.class.php + +# Compile standard input and write to standard output. +$ echo " Date: Sun, 15 Nov 2020 14:22:35 +0100 Subject: [PATCH 311/926] Add support for explicit octal integer literal notation See https://wiki.php.net/rfc/explicit_octal_notation --- 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 | 2 +- .../ast/emit/RewriteExplicitOctals.class.php | 17 +++++++++++++++++ .../ast/unittest/emit/ScalarsTest.class.php | 12 +++++++++++- 7 files changed, 33 insertions(+), 6 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 49607f4d..5f63c165 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -9,7 +9,7 @@ */ class PHP70 extends PHP { use OmitPropertyTypes, OmitConstModifiers; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 3f2c025c..1465f953 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -9,7 +9,7 @@ */ class PHP71 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 93a6c431..32b70004 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -9,7 +9,7 @@ */ class PHP72 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 50e4b433..37995211 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index dc14f9bc..99a2e456 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php new file mode 100755 index 00000000..ee0cab19 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php @@ -0,0 +1,17 @@ + `016`. + * + * @see https://wiki.php.net/rfc/explicit_octal_notation + */ +trait RewriteExplicitOctals { + + protected function emitLiteral($result, $literal) { + if (0 === strncasecmp($literal->expression, '0o', 2)) { + $result->out->write('0'.substr($literal->expression, 2)); + } else { + $result->out->write($literal->expression); + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index 69a089fc..5466abf8 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -4,11 +4,21 @@ class ScalarsTest extends EmittingTest { - #[Test, Values([['0', 0], ['1', 1], ['-1', -1], ['0xff', 255], ['0755', 493], ['1.5', 1.5], ['-1.5', -1.5],])] + #[Test, Values([['0', 0], ['1', 1], ['-1', -1], ['1.5', 1.5], ['-1.5', -1.5],])] public function numbers($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } + #[Test, Values([['0x0', 0], ['0xff', 255], ['0xFF', 255], ['0XFF', 255]])] + public function hexadecimal_numbers($literal, $result) { + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + } + + #[Test, Values([['0755', 493], ['0o16', 14], ['0O16', 14]])] + public function octal_numbers($literal, $result) { + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + } + #[Test, Values([['135_99', 13599], ['107_925_284.88', 107925284.88], ['0xCAFE_F00D', 3405705229], ['0b0101_1111', 95], ['0137_041', 48673],])] public function numeric_literal_separator($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); From 1d0ee4f9ebf0ecc5602ad4953629076fddfe632d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 14:25:02 +0100 Subject: [PATCH 312/926] Use string comparison instead of invoking strncasecmp() --- src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php index ee0cab19..f44f61e2 100755 --- a/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php +++ b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php @@ -8,7 +8,7 @@ trait RewriteExplicitOctals { protected function emitLiteral($result, $literal) { - if (0 === strncasecmp($literal->expression, '0o', 2)) { + if ('0' === $literal->expression[0] && ($c= $literal->expression[1] ?? null) && ('o' === $c || 'O' === $c)) { $result->out->write('0'.substr($literal->expression, 2)); } else { $result->out->write($literal->expression); From 6376ab4c6e5fee81ec81bd47ec35489ba6aeda83 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 15 Nov 2020 14:29:02 +0100 Subject: [PATCH 313/926] Add test for binary notation while we're at it --- src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index 5466abf8..d5ea802b 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -9,6 +9,11 @@ public function numbers($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); } + #[Test, Values([['0b0', 0], ['0b10', 2], ['0B10', 2]])] + public function binary_numbers($literal, $result) { + Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + } + #[Test, Values([['0x0', 0], ['0xff', 255], ['0xFF', 255], ['0XFF', 255]])] public function hexadecimal_numbers($literal, $result) { Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); From b43150adef90ed32936c68a636588e68bbabcf1f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 19 Nov 2020 21:32:37 +0100 Subject: [PATCH 314/926] Add PHP 8 badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc6974e5..9e1e3eea 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ XP Compiler [![Build Status on TravisCI](https://secure.travis-ci.org/xp-forge/sequence.svg)](http://travis-ci.org/xp-framework/compiler) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) -[![Supports PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.png)](http://php.net/) +[![Requires PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.svg)](http://php.net/) +[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/) [![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) Compiles future PHP to today's PHP. From a71c218726579fda11bf5dbcd3197491696f7708 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 20 Nov 2020 17:59:06 +0100 Subject: [PATCH 315/926] Add test for lang.ast.Result --- .../lang/ast/unittest/ResultTest.class.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/ResultTest.class.php diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php new file mode 100755 index 00000000..bd25c377 --- /dev/null +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -0,0 +1,44 @@ +temp(), $r->temp(), $r->temp()]); + } + + #[Test] + public function writes_php_open_tag_as_default_preable() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out)); + Assert::equals('bytes()); + } + + #[Test, Values(['', 'bytes()); + } + + #[Test] + public function writing_via_buffer_does_not_affect_underlying_output() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out)); + $r->buffer(function($r) { + $r->out->write('namespace test;'); + $r->out->write('use unittest\Assert;'); + }); + Assert::equals('bytes()); + } +} \ No newline at end of file From 97856ea980cad61367d051b23cea34501590cdc6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 20 Nov 2020 18:40:37 +0100 Subject: [PATCH 316/926] Test loading class bytes and class dependencies --- .../loader/CompilingClassLoaderTest.class.php | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index ae00031a..fee99e34 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -13,23 +13,28 @@ static function __static() { } /** - * Loads a class from source + * Sets us compiling class loader with a given type and source code, then + * executes callback. * - * @param string $type - * @param string $source - * @return lang.XPClass + * @param [:string] $source + * @param function(lang.IClassLoader, string): var $callback + * @return var */ - private function load($type, $source) { + private function compile($source, $callback) { $namespace= 'ns'.uniqid(); $folder= new Folder(Environment::tempDir(), $namespace); $folder->exists() || $folder->create(); - FileUtil::setContents(new File($folder, $type.'.php'), sprintf($source, $namespace)); + $names= []; + foreach ($source as $type => $code) { + FileUtil::setContents(new File($folder, $type.'.php'), sprintf($code, $namespace)); + $names[$type]= $namespace.'.'.$type; + } $cl= ClassLoader::registerPath($folder->path); $loader= CompilingClassLoader::instanceFor(self::$runtime); try { - return $loader->loadClass($namespace.'.'.$type); + return $callback($loader, $names); } finally { ClassLoader::removeLoader($cl); $folder->unlink(); @@ -43,21 +48,49 @@ public function can_create() { #[Test] public function load_class() { - Assert::equals('Tests', $this->load('Tests', 'getSimpleName()); + Assert::equals('Tests', $this->compile(['Tests' => 'loadClass($types['Tests'])->getSimpleName(); + })); + } + + #[Test] + public function load_dependencies() { + $source= [ + 'Child' => ' ' ' 'compile($source, function($loader, $types) { return $loader->loadClass($types['Child']); }); + $n= function($c) { return $c->getSimpleName(); }; + Assert::equals( + ['Child', 'Base', ['Impl'], ['Feature']], + [$n($c), $n($c->getParentClass()), array_map($n, $c->getInterfaces()), array_map($n, $c->getTraits())] + ); + } + + #[Test] + public function load_class_bytes() { + $code= $this->compile(['Tests' => 'loadClassBytes($types['Tests']); + }); + Assert::true((bool)preg_match('/<\?php .+ class Tests/', $code)); } #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => 'Compiler error: Expected "{", have "(end)"'])] public function load_class_with_syntax_errors() { - $this->load('Errors', "compile(['Errors' => "loadClass($types['Errors']); }); } #[Test] public function triggered_errors_filename() { - $t= $this->load('Triggers', ' 'compile($source, function($loader, $types) { return $loader->loadClass($types['Triggers']); }); $t->newInstance()->trigger(); Assert::notEquals(false, strpos( From a0914457a682fe277929f3b3921898a7417b9b39 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 20 Nov 2020 18:56:04 +0100 Subject: [PATCH 317/926] Test various transformation outcomes --- .../lang/ast/unittest/EmitterTest.class.php | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 417d21d0..39c75a4f 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -1,8 +1,9 @@ newEmitter(); } + #[Test, Expect(IllegalArgumentException::class)] + public function cannot_create_for_unsupported_php_version() { + Emitter::forRuntime('PHP.4.3.0'); + } + #[Test] public function transformations_initially_empty() { Assert::equals([], $this->newEmitter()->transformations()); @@ -32,8 +38,8 @@ public function transform() { #[Test] public function remove() { - $first= function($class) { return $class; }; - $second= function($class) { $class->annotations['author']= 'Test'; return $class; }; + $first= function($codegen, $class) { return $class; }; + $second= function($codegen, $class) { $class->annotations['author']= 'Test'; return $class; }; $fixture= $this->newEmitter(); $transformation= $fixture->transform('class', $first); @@ -44,7 +50,7 @@ public function remove() { #[Test] public function remove_unsets_empty_kind() { - $function= function($class) { return $class; }; + $function= function($codegen, $class) { return $class; }; $fixture= $this->newEmitter(); $transformation= $fixture->transform('class', $function); @@ -58,4 +64,44 @@ public function emit_node_without_kind() { public $kind= null; }); } + + #[Test] + public function transform_modifying_node() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { $var->name= '_'.$var->name; return $var; }); + $out= new MemoryOutputStream(); + $fixture->emitOne(new Result($out), new Variable('a')); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_node() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { return new Code('$variables["'.$var->name.'"]'); }); + $out= new MemoryOutputStream(); + $fixture->emitOne(new Result($out), new Variable('a')); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_array() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { return [new Code('$variables["'.$var->name.'"]')]; }); + $out= new MemoryOutputStream(); + $fixture->emitOne(new Result($out), new Variable('a')); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_null() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { return null; }); + $out= new MemoryOutputStream(); + $fixture->emitOne(new Result($out), new Variable('a')); + + Assert::equals('bytes()); + } } \ No newline at end of file From e30852d818037772d399465d4bd091d7f7b193d1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 20 Nov 2020 19:18:25 +0100 Subject: [PATCH 318/926] Add tests for loading URIs and resources --- .../loader/CompilingClassLoaderTest.class.php | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index fee99e34..af1613a8 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -2,7 +2,7 @@ use io\{File, FileUtil, Folder}; use lang\ast\CompilingClassLoader; -use lang\{ClassFormatException, ClassLoader, Environment}; +use lang\{ClassFormatException, ElementNotFoundException, ClassLoader, Environment}; use unittest\{Assert, Expect, Test, TestCase}; class CompilingClassLoaderTest { @@ -34,7 +34,7 @@ private function compile($source, $callback) { $loader= CompilingClassLoader::instanceFor(self::$runtime); try { - return $callback($loader, $names); + return $callback($loader, $names, $cl); } finally { ClassLoader::removeLoader($cl); $folder->unlink(); @@ -46,6 +46,21 @@ public function can_create() { CompilingClassLoader::instanceFor(self::$runtime); } + #[Test, Values(['7.0.0', '7.0.1', '7.1.0', '7.2.0', '7.3.0', '7.4.0', '7.4.12', '8.0.0'])] + public function supports_php($version) { + CompilingClassLoader::instanceFor('PHP.'.$version); + } + + #[Test] + public function string_representation() { + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('PHP.7.0.0')->toString()); + } + + #[Test] + public function hashcode() { + Assert::equals('CPHP70', CompilingClassLoader::instanceFor('PHP.7.0.0')->hashCode()); + } + #[Test] public function load_class() { Assert::equals('Tests', $this->compile(['Tests' => 'compile(['Tests' => 'loadUri($temp->path.strtr($types['Tests'], '.', DIRECTORY_SEPARATOR).CompilingClassLoader::EXTENSION); + }); + Assert::equals('Tests', $class->getSimpleName()); + } + #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => 'Compiler error: Expected "{", have "(end)"'])] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); }); @@ -99,4 +122,29 @@ public function trigger() { )); \xp::gc(); } + + #[Test] + public function does_not_provide_non_existant_uri() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesUri('NotFound.php')); + } + + #[Test] + public function does_not_provide_non_existant_resource() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesResource('notfound.md')); + } + + #[Test] + public function does_not_provide_non_existant_package() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesPackage('notfound')); + } + + #[Test, Expect(ElementNotFoundException::class)] + public function loading_non_existant_resource() { + CompilingClassLoader::instanceFor(self::$runtime)->getResource('notfound.md'); + } + + #[Test, Expect(ElementNotFoundException::class)] + public function loading_non_existant_resource_as_stream() { + CompilingClassLoader::instanceFor(self::$runtime)->getResourceAsStream('notfound.md'); + } } \ No newline at end of file From b86f87b252e65e400d9f5710c7cd6456dc521c07 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 20 Nov 2020 19:47:08 +0100 Subject: [PATCH 319/926] Extensively test type literal emitting --- src/main/php/lang/ast/emit/PHP.class.php | 20 +-- .../ast/unittest/TypeLiteralsTest.class.php | 114 ++++++++++++++++++ 2 files changed, 124 insertions(+), 10 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0538c4fb..7c21a62b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -11,23 +11,23 @@ abstract class PHP extends Emitter { protected $literals= []; /** - * Returns the simple name for use in a declaration + * Emit type literal or NULL if no type should be emitted * - * @param string $name E.g. `\lang\ast\Parse` - * @return string In the above example, `Parse`. + * @param ?lang.ast.Type $type + * @return ?string */ - protected function declaration($name) { - return substr($name, strrpos($name, '\\') + 1); + public function literal($type) { + return null === $type ? null : $this->literals[get_class($type)]($type); } /** - * Emit type literal or NULL if no type should be emitted + * Returns the simple name for use in a declaration * - * @param ?lang.ast.Type $type - * @return ?string + * @param string $name E.g. `\lang\ast\Parse` + * @return string In the above example, `Parse`. */ - protected function literal($type) { - return null === $type ? null : $this->literals[get_class($type)]($type); + protected function declaration($name) { + return substr($name, strrpos($name, '\\') + 1); } /** diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php new file mode 100755 index 00000000..2d2141d6 --- /dev/null +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -0,0 +1,114 @@ +base(); + yield [new IsLiteral('object'), null]; + yield [new IsLiteral('void'), null]; + yield [new IsLiteral('iterable'), null]; + yield [new IsLiteral('mixed'), null]; + yield [new IsNullable(new IsLiteral('string')), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + } + + /** + * PHP 7.1 added `void` and `iterable` as well as support for nullable types + * + * @return iterable + */ + private function php71() { + yield from $this->base(); + yield [new IsLiteral('object'), null]; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), null]; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + } + + /** + * PHP 7.2 added `object` + * + * @return iterable + */ + private function php72() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), null]; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + } + + /** + * PHP 7.4 is the same as PHP 7.2 + * + * @return iterable + */ + private function php74() { + yield from $this->php72(); + } + + /** + * PHP 8.0 added `mixed` and union types + * + * @return iterable + */ + private function php80() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + } + + #[Test, Values('php70')] + public function php70_literals($type, $literal) { + Assert::equals($literal, (new PHP70())->literal($type)); + } + + #[Test, Values('php71')] + public function php71_literals($type, $literal) { + Assert::equals($literal, (new PHP71())->literal($type)); + } + + #[Test, Values('php72')] + public function php72_literals($type, $literal) { + Assert::equals($literal, (new PHP72())->literal($type)); + } + + #[Test, Values('php74')] + public function php74_literals($type, $literal) { + Assert::equals($literal, (new PHP74())->literal($type)); + } + + #[Test, Values('php80')] + public function php80_literals($type, $literal) { + Assert::equals($literal, (new PHP80())->literal($type)); + } +} \ No newline at end of file From 86a6be92ba339a6af6937c42ab206fb98b6c5a38 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 13:12:54 +0100 Subject: [PATCH 320/926] Test function self references via `use(&$var)` See https://externals.io/message/112216 --- .../unittest/emit/InvocationTest.class.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index b19e5c2f..17ac561f 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -104,6 +104,25 @@ public function run() { )); } + #[Test] + public function function_self_reference() { + Assert::equals(13, $this->run( + 'class { + + public function run() { + $fib= function($i) use(&$fib) { + if (0 === $i || 1 === $i) { + return $i; + } else { + return $fib($i - 1) + $fib($i - 2); + } + }; + return $fib(7); + } + }' + )); + } + #[Test, Values(['"html(<) = <", flags: ENT_HTML5', '"html(<) = <", ENT_HTML5, double: true', 'string: "html(<) = <", flags: ENT_HTML5', 'string: "html(<) = <", flags: ENT_HTML5, double: true',])] public function named_arguments_in_exact_order($arguments) { Assert::equals('html(<) = &lt;', $this->run( From cb0a1cdfef36164b012ad29a6cfe8dbb67bbb690 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 13:29:40 +0100 Subject: [PATCH 321/926] Allow dropping expression from match See https://wiki.php.net/rfc/match_expression_v2#allow_dropping_true: $result = match { ... }; // Equivalent to $result = match (true) { ... }; --- src/main/php/lang/ast/emit/PHP.class.php | 8 ++++++-- src/main/php/lang/ast/emit/PHP80.class.php | 10 +++++++--- .../emit/ControlStructuresTest.class.php | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7c21a62b..a7b706bb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -561,8 +561,12 @@ protected function emitSwitch($result, $switch) { protected function emitMatch($result, $match) { $t= $result->temp(); - $result->out->write('('.$t.'='); - $this->emitOne($result, $match->expression); + if (null === $match->expression) { + $result->out->write('('.$t.'=true'); + } else { + $result->out->write('('.$t.'='); + $this->emitOne($result, $match->expression); + } $result->out->write(')'); $b= 0; diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index dc14f9bc..b1cce23d 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -55,9 +55,13 @@ protected function emitNew($result, $new) { } protected function emitMatch($result, $match) { - $result->out->write('match ('); - $this->emitOne($result, $match->expression); - $result->out->write(') {'); + if (null === $match->expression) { + $result->out->write('match (true) {'); + } else { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + } foreach ($match->cases as $case) { $b= 0; diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 3411f8c1..c9a0d741 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -84,6 +84,22 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test] + public function match_allows_dropping_true() { + $r= $this->run('class { + public function run($arg) { + return match { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }', 10); + + Assert::equals('10+ items', $r); + } + #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] public function unhandled_match() { $this->run('class { From 8bc46f8bd35c667d0f1258adcd7ae4d56332c91a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 13:31:31 +0100 Subject: [PATCH 322/926] Add dependency on change in xp-framework/ast library --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 37860694..8b277e7f 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^6.0 | ^5.2", + "xp-framework/ast": "dev-feature/match_without_expression as 6.1.0 | ^6.0 | ^5.2", "php" : ">=7.0.0" }, "require-dev" : { From 432b9a6eda97ebc98ead4da3365fd1d218a94276 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 13:35:01 +0100 Subject: [PATCH 323/926] Fix composer error: Aliases should be in the form "exact-version as other-exact-version" --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8b277e7f..c1c75a25 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/match_without_expression as 6.1.0 | ^6.0 | ^5.2", + "xp-framework/ast": "dev-feature/match_without_expression as 6.1.0", "php" : ">=7.0.0" }, "require-dev" : { From 43e630eada8e57eb72876488c66854923c39e558 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 13:39:12 +0100 Subject: [PATCH 324/926] Slightly optimize match without expression for PHP 7.x --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a7b706bb..53331fbf 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -562,12 +562,12 @@ protected function emitSwitch($result, $switch) { protected function emitMatch($result, $match) { $t= $result->temp(); if (null === $match->expression) { - $result->out->write('('.$t.'=true'); + $result->out->write('('.$t.'=true)'); } else { $result->out->write('('.$t.'='); $this->emitOne($result, $match->expression); + $result->out->write(')'); } - $result->out->write(')'); $b= 0; foreach ($match->cases as $case) { From e6d2c890e91d7ce6095a3e27cde5a79d5350f1b2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 21 Nov 2020 18:19:58 +0100 Subject: [PATCH 325/926] Test emitting traits and interfaces with members --- .../ast/unittest/emit/TypeDeclarationTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index d23f945e..508167cb 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -43,11 +43,21 @@ public function trait_type() { Assert::true($this->type('trait { }')->isTrait()); } + #[Test] + public function trait_type_with_method() { + Assert::true($this->type('trait { public function name() { return "Test"; }}')->isTrait()); + } + #[Test] public function interface_type() { Assert::true($this->type('interface { }')->isInterface()); } + #[Test] + public function interface_type_with_method() { + Assert::true($this->type('interface { public function name(); }')->isInterface()); + } + #[Test, Values(['public', 'private', 'protected'])] public function constant($modifiers) { $c= $this->type('class { '.$modifiers.' const test = 1; }')->getConstant('test'); From ca3548401048c420491078ab999d8cc64ba331cf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 12:07:35 +0100 Subject: [PATCH 326/926] Add support for non-capturing catches See https://wiki.php.net/rfc/non-capturing_catches --- ChangeLog.md | 6 ++++++ composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 5 +++-- .../ast/unittest/emit/ExceptionsTest.class.php | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 88069c9a..86327d94 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.6.0 / 2020-11-22 + +* Added support for non-capturing catches, see this PHP 8 RFC: + https://wiki.php.net/rfc/non-capturing_catches + (@thekid) + ## 5.5.0 / 2020-11-15 * Merged PR #91 - Refactor rewriting type literals: diff --git a/composer.json b/composer.json index 37860694..4c44581f 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^6.0 | ^5.2", + "xp-framework/ast": "^6.1 | ^5.2", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7c21a62b..87cea1d5 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -588,10 +588,11 @@ protected function emitMatch($result, $match) { } protected function emitCatch($result, $catch) { + $capture= $catch->variable ? '$'.$catch->variable : $result->temp(); if (empty($catch->types)) { - $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); + $result->out->write('catch(\\Throwable '.$capture.') {'); } else { - $result->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); + $result->out->write('catch('.implode('|', $catch->types).' '.$capture.') {'); } $this->emitAll($result, $catch->body); $result->out->write('}'); diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index b0cd6a73..cb6c57e8 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -50,6 +50,21 @@ public function run() { Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } + #[Test] + public function non_capturing_catch() { + $t= $this->type('class { + public function run() { + try { + throw new \\lang\\IllegalArgumentException("test"); + } catch (\\lang\\IllegalArgumentException) { + return "Expected"; + } + } + }'); + + Assert::equals('Expected', $t->newInstance()->run()); + } + #[Test] public function finally_without_exception() { $t= $this->type('class { From 4e720ea07350ee89eb4cdce52660e301bdfaa60f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 12:11:30 +0100 Subject: [PATCH 327/926] Fix PHP 7.0 compatibility for non-capturing catches --- src/main/php/lang/ast/emit/RewriteMultiCatch.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php index 648acc80..ea1bd69e 100755 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php @@ -8,15 +8,16 @@ trait RewriteMultiCatch { protected function emitCatch($result, $catch) { + $capture= $catch->variable ? '$'.$catch->variable : $result->temp(); if (empty($catch->types)) { - $result->out->write('catch(\\Throwable $'.$catch->variable.') {'); + $result->out->write('catch(\\Throwable '.$capture.') {'); } else { $last= array_pop($catch->types); $label= sprintf('c%u', crc32($last)); foreach ($catch->types as $type) { - $result->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); + $result->out->write('catch('.$type.' '.$capture.') { goto '.$label.'; }'); } - $result->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); + $result->out->write('catch('.$last.' '.$capture.') { '.$label.':'); } $this->emitAll($result, $catch->body); From 56f6df761fefe1026b10fe50cbe191d6ba23c1d9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 12:11:53 +0100 Subject: [PATCH 328/926] Optimize PHP 8 for non-capturing catches --- src/main/php/lang/ast/emit/PHP80.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index dc14f9bc..9af81215 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -54,6 +54,17 @@ protected function emitNew($result, $new) { $result->out->write(')'); } + protected function emitCatch($result, $catch) { + $capture= $catch->variable ? ' $'.$catch->variable : ''; + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable'.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + protected function emitMatch($result, $match) { $result->out->write('match ('); $this->emitOne($result, $match->expression); From 778e252a635a7153911fdd795498f7253c24756f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 12:13:56 +0100 Subject: [PATCH 329/926] Add PHP 8.1, no longer allow PHP 8.0 to fail --- .github/workflows/ci.yml | 4 ++-- ChangeLog.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95950b9d..dd2d40b3 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,11 +10,11 @@ jobs: tests: name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions == '8.0' }} + continue-on-error: ${{ matrix.php-versions == '8.1' }} strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] os: [ubuntu-latest, windows-latest] steps: diff --git a/ChangeLog.md b/ChangeLog.md index 86327d94..cd1be3bb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,8 @@ XP Compiler ChangeLog ## 5.6.0 / 2020-11-22 +* Added PHP 8.1-dev to test matrix now that is has been branched + (@thekid) * Added support for non-capturing catches, see this PHP 8 RFC: https://wiki.php.net/rfc/non-capturing_catches (@thekid) From aba7f465c929433b46734b654405745de1611fc2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 12:33:26 +0100 Subject: [PATCH 330/926] Optimize null-safe instance operator for PHP 8.0 Feature is now built-in, see https://wiki.php.net/rfc/nullsafe_operator --- ChangeLog.md | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index cd1be3bb..a502d710 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 5.6.0 / 2020-11-22 +* Optimized null-safe instance operator for PHP 8.0 - @thekid * Added PHP 8.1-dev to test matrix now that is has been branched (@thekid) * Added support for non-capturing catches, see this PHP 8 RFC: diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 9af81215..4c3b2664 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -65,6 +65,19 @@ protected function emitCatch($result, $catch) { $result->out->write('}'); } + protected function emitNullsafeInstance($result, $instance) { + $this->emitOne($result, $instance->expression); + $result->out->write('?->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + protected function emitMatch($result, $match) { $result->out->write('match ('); $this->emitOne($result, $match->expression); From 3b855e0318394e2b9b1fa052926f21d1bc91470d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 16:34:41 +0100 Subject: [PATCH 331/926] Add support for static return type See https://wiki.php.net/rfc/static_return_type, implemented in PHP 8.0 For PHP 7, emit "self" as return type instead, almost the same --- 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 +- .../ast/unittest/emit/MembersTest.class.php | 26 +++++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 49607f4d..7bd1aa01 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -17,7 +17,7 @@ public function __construct() { IsFunction::class => function($t) { return 'callable'; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { return $t->literal(); }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, IsNullable::class => function($t) { return null; }, IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 3f2c025c..9ac787a9 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -17,7 +17,7 @@ public function __construct() { IsFunction::class => function($t) { return 'callable'; }, IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { return $t->literal(); }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 93a6c431..3f8cf6b7 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -17,7 +17,7 @@ public function __construct() { IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { return $t->literal(); }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 50e4b433..e5d1634e 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -16,7 +16,7 @@ public function __construct() { IsArray::class => function($t) { return 'array'; }, IsMap::class => function($t) { return 'array'; }, IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { return $t->literal(); }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 936643a1..6807e5ef 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,5 +1,6 @@ type(' + class { public function run(): self { return $this; } } + '); + Assert::equals($t, $t->getMethod('run')->getReturnType()); + } + + #[Test] + public function static_return_type() { + $t= $this->type(' + class Base { public function run(): static { return $this; } } + class extends Base { } + '); + Assert::equals($t, $t->getMethod('run')->getReturnType()); + } + + #[Test] + public function array_of_self_return_type() { + $t= $this->type(' + class { public function run(): array { return [$this]; } } + '); + Assert::equals(new ArrayType($t), $t->getMethod('run')->getReturnType()); + } } \ No newline at end of file From b8f7e730de16612de3a784baa51871881640378b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 16:47:37 +0100 Subject: [PATCH 332/926] Verify ::class works on objects See https://wiki.php.net/rfc/class_name_literal_on_object, implemented in PHP 8.0 --- ChangeLog.md | 1 + .../unittest/emit/ClassLiteralTest.class.php | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php diff --git a/ChangeLog.md b/ChangeLog.md index a502d710..f9b42f81 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ XP Compiler ChangeLog ## 5.6.0 / 2020-11-22 +* Merged PR #94: Add support for `static` return type - @thekid * Optimized null-safe instance operator for PHP 8.0 - @thekid * Added PHP 8.1-dev to test matrix now that is has been branched (@thekid) diff --git a/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php new file mode 100755 index 00000000..ad0ab186 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php @@ -0,0 +1,52 @@ +run('use lang\Primitive; class { + public function run() { return Primitive::class; } + }'); + Assert::equals(Primitive::class, $r); + } + + #[Test] + public function on_self() { + $t= $this->type('class { + public function run() { return self::class; } + }'); + Assert::equals($t->getName(), $t->newInstance()->run()); + } + + #[Test] + public function on_object() { + $t= $this->type('class { + public function run() { return $this::class; } + }'); + Assert::equals($t->getName(), $t->newInstance()->run()); + } + + #[Test] + public function on_instantiation() { + $t= $this->type('class { + public function run() { return new self()::class; } + }'); + Assert::equals($t->getName(), $t->newInstance()->run()); + } + + #[Test] + public function on_braced_expression() { + $t= $this->type('class { + public function run() { return (new self())::class; } + }'); + Assert::equals($t->getName(), $t->newInstance()->run()); + } +} \ No newline at end of file From c82a8478ad1738ec136ccb95a51311a940641d7f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 16:53:56 +0100 Subject: [PATCH 333/926] Verify match statement with multiple values per case --- .../emit/ControlStructuresTest.class.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 3411f8c1..82f0dbed 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -53,7 +53,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items']])] public function match($input, $expected) { $r= $this->run('class { public function run($arg) { @@ -68,6 +68,21 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test, Values([[200, 'OK'], [302, 'Redirect'], [404, 'Error #404']])] + public function match_with_multiple($input, $expected) { + $r= $this->run('class { + public function run($arg) { + return match ($arg) { + 200, 201, 202, 203, 204 => "OK", + 300, 301, 302, 303, 307 => "Redirect", + default => "Error #$arg", + }; + } + }', $input); + + Assert::equals($expected, $r); + } + #[Test, Values([[0, 'no items'], [1, 'one item'], [5, '5 items'], [10, '10+ items'],])] public function match_with_binary($input, $expected) { $r= $this->run('class { From bd67465c88d65a0a54047f2f698c203ebed5a8d3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Nov 2020 19:31:32 +0100 Subject: [PATCH 334/926] Support compiling to XAR archives --- .../php/xp/compiler/CompileRunner.class.php | 15 +++++-- src/main/php/xp/compiler/Output.class.php | 6 +++ src/main/php/xp/compiler/ToArchive.class.php | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100755 src/main/php/xp/compiler/ToArchive.class.php diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 6fa30e50..d872bfab 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -22,6 +22,10 @@ * ```sh * $ xp compile -o dist src/main/php/ src/test/php/ * ``` + * - Compile `src/main/php` and `src/test/php` to the `dist.xar` archive. + * ```sh + * $ xp compile -o dist.xar src/main/php/ src/test/php/ + * ``` * - Compile `src/main/php`, do not write output * ```sh * $ xp compile -n src/main/php/ @@ -73,12 +77,13 @@ public static function main(array $args) { $t= new Timer(); $total= $errors= 0; $time= 0.0; - foreach ($input as $path => $in) { + foreach ($input as $path => $source) { $file= $path->toString('/'); $t->start(); try { - $parse= $lang->parse(new Tokens($in, $file)); - $emit->emitAll(new Result($output->target((string)$path)), $parse->stream()); + $parse= $lang->parse(new Tokens($source, $file)); + $target= $output->target((string)$path); + $emit->emitAll(new Result($target), $parse->stream()); $t->stop(); Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); @@ -89,10 +94,12 @@ public static function main(array $args) { } finally { $total++; $time+= $t->elapsedTime(); - $in->close(); + $source->close(); + $target->close(); } } + $output->close(); Console::$err->writeLine(); Console::$err->writeLinef( "%s Compiled %d file(s) to %s using %s, %d error(s) occurred\033[0m", diff --git a/src/main/php/xp/compiler/Output.class.php b/src/main/php/xp/compiler/Output.class.php index f257ad34..7a4c2e8d 100755 --- a/src/main/php/xp/compiler/Output.class.php +++ b/src/main/php/xp/compiler/Output.class.php @@ -17,6 +17,8 @@ public static function newInstance($arg) { return new ToStream(Console::$out->getStream()); } else if (strstr($arg, '.php')) { return new ToFile($arg); + } else if (strstr($arg, '.xar')) { + return new ToArchive($arg); } else { return new ToFolder($arg); } @@ -30,4 +32,8 @@ public static function newInstance($arg) { */ public abstract function target($name); + /** @return void */ + public function close() { + // NOOP + } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/ToArchive.class.php b/src/main/php/xp/compiler/ToArchive.class.php new file mode 100755 index 00000000..dca6901f --- /dev/null +++ b/src/main/php/xp/compiler/ToArchive.class.php @@ -0,0 +1,40 @@ +archive= new Archive($file instanceof File ? $file : new File($file)); + $this->archive->open(Archive::CREATE); + } + + /** + * Returns the target for a given input + * + * @param string $name + * @return io.streams.OutputStream + */ + public function target($name) { + return new class($this->archive, $name) implements OutputStream { + private static $replace= [DIRECTORY_SEPARATOR => '/', '.php' => \xp::CLASS_FILE_EXT]; + + private $bytes= ''; + private $archive, $name; + + public function __construct($archive, $name) { $this->archive= $archive; $this->name= $name; } + public function write($bytes) { $this->bytes.= $bytes; } + public function flush() { } + public function close() { $this->archive->addBytes(strtr($this->name, self::$replace), $this->bytes); } + }; + } + + /** @return void */ + public function close() { + $this->archive->create(); + } +} \ No newline at end of file From e62d8df05ce8fd5d22db8429ea5b5248dde0ec7f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 23 Nov 2020 20:29:28 +0100 Subject: [PATCH 335/926] QA: Apidocs / WS --- src/main/php/xp/compiler/ToArchive.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/ToArchive.class.php b/src/main/php/xp/compiler/ToArchive.class.php index dca6901f..6c10a558 100755 --- a/src/main/php/xp/compiler/ToArchive.class.php +++ b/src/main/php/xp/compiler/ToArchive.class.php @@ -4,6 +4,7 @@ use io\streams\OutputStream; use lang\archive\Archive; +/** Writes compiled files to a .XAR archive */ class ToArchive extends Output { private $archive; @@ -22,7 +23,6 @@ public function __construct($file) { public function target($name) { return new class($this->archive, $name) implements OutputStream { private static $replace= [DIRECTORY_SEPARATOR => '/', '.php' => \xp::CLASS_FILE_EXT]; - private $bytes= ''; private $archive, $name; From 4e474e01790854007d8b8690fa9676096e2caffc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 23 Nov 2020 21:16:48 +0100 Subject: [PATCH 336/926] Use XP runners, version 8.2.0 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd2d40b3..56527fb0 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run && + curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.2.0.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From f3fbf62831518ec1494d8d0515e4d61679e6c574 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 26 Nov 2020 22:27:43 +0100 Subject: [PATCH 337/926] Release 5.7.0 --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f9b42f81..8c3f190c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 5.7.0 / 2020-11-26 + +* Verified full PHP 8 support now that PHP 8.0.0 has been released, + see https://www.php.net/archive/2020.php#2020-11-26-3 + (@thekid) +* Merged PR #95: Support compiling to XAR archives - @thekid + ## 5.6.0 / 2020-11-22 * Merged PR #94: Add support for `static` return type - @thekid From f8919be59b5424cdf569b14f5dd3846eaf73f80c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 14:27:56 +0100 Subject: [PATCH 338/926] Enclose blocks where PHP only allows expressions This not only allows fn() => { ... } but also match ($arg) { 200 => { ... }} --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 106 +++++++++++++----- src/main/php/lang/ast/emit/PHP80.class.php | 12 +- .../RewriteBlockLambdaExpressions.class.php | 18 ++- .../emit/RewriteLambdaExpressions.class.php | 46 ++------ .../emit/ControlStructuresTest.class.php | 23 +++- 6 files changed, 133 insertions(+), 74 deletions(-) diff --git a/composer.json b/composer.json index 4c44581f..9234c59a 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^6.1 | ^5.2", + "xp-framework/ast": "dev-refactor/block-expressions as 7.0.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 87cea1d5..a2ee150d 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,6 +1,6 @@ codegen->search($node, 'variable') as $var) { + if (isset($result->locals[$var->name])) { + $capture[$var->name]= true; + } + } + unset($capture['this']); + + if ($signature) { + $result->out->write('function'); + $this->emitSignature($result, $signature); + foreach ($signature->parameters as $param) { + unset($capture[$param->name]); + } + } else { + $result->out->write('function()'); + } + + $result->stack[]= $result->locals; + $result->locals= []; + if ($capture) { + $result->out->write('use($'.implode(', $', array_keys($capture)).')'); + foreach ($capture as $name => $_) { + $result->locals[$name]= true; + } + } + + $result->out->write('{'); + $emit($result, $node); + $result->out->write('}'); + $result->locals= array_pop($result->stack); + } + + /** + * Convert blocks to IIFEs to allow a list of statements where PHP syntactically + * doesn't, e.g. `fn`-style lambdas or match expressions. + * + * @param lang.ast.Result $result + * @param lang.ast.Node $expression + * @return void + */ + protected function emitAsExpression($result, $expression) { + if ($expression instanceof Block) { + $result->out->write('('); + $this->enclose($result, $expression, null, function($result, $expression) { + $this->emitAll($result, $expression->statements); + }); + $result->out->write(')()'); + } else { + $this->emitOne($result, $expression); + } + } + protected function emitStart($result, $start) { // NOOP } @@ -78,10 +140,9 @@ protected function emitLiteral($result, $literal) { protected function emitEcho($result, $echo) { $result->out->write('echo '); - $s= sizeof($echo->expressions) - 1; foreach ($echo->expressions as $i => $expr) { + if ($i++) $result->out->write(','); $this->emitOne($result, $expr); - if ($i < $s) $result->out->write(','); } } @@ -194,10 +255,9 @@ protected function emitParameter($result, $parameter) { protected function emitSignature($result, $signature) { $result->out->write('('); - $s= sizeof($signature->parameters) - 1; foreach ($signature->parameters as $i => $parameter) { + if ($i++) $result->out->write(','); $this->emitParameter($result, $parameter); - if ($i < $s) $result->out->write(', '); } $result->out->write(')'); @@ -244,14 +304,7 @@ protected function emitLambda($result, $lambda) { $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); $result->out->write('=>'); - - if (is_array($lambda->body)) { - $result->out->write('{'); - $this->emitAll($result, $lambda->body); - $result->out->write('}'); - } else { - $this->emitOne($result, $lambda->body); - } + $this->emitOne($result, $lambda->body); } protected function emitClass($result, $class) { @@ -572,7 +625,7 @@ protected function emitMatch($result, $match) { $result->out->write('===('); $this->emitOne($result, $expression); $result->out->write(')?'); - $this->emitOne($result, $case->body); + $this->emitAsExpression($result, $case->body); $result->out->write(':('); $b++; } @@ -582,7 +635,7 @@ protected function emitMatch($result, $match) { if (null === $match->default) { $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); } else { - $this->emitOne($result, $match->default); + $this->emitAsExpression($result, $match->default); } $result->out->write(str_repeat(')', $b)); } @@ -621,19 +674,13 @@ protected function emitThrow($result, $throw) { } protected function emitThrowExpression($result, $throw) { - $capture= []; - foreach ($result->codegen->search($throw->expression, 'variable') as $var) { - if (isset($result->locals[$var->name])) { - $capture[$var->name]= true; - } - } - unset($capture['this']); - - $result->out->write('(function()'); - $capture && $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $result->out->write('{ throw '); - $this->emitOne($result, $throw->expression); - $result->out->write('; })()'); + $result->out->write('('); + $this->enclose($result, $throw->expression, null, function($result, $expression) { + $result->out->write('throw '); + $this->emitOne($result, $expression); + $result->out->write(';'); + }); + $result->out->write(')()'); } protected function emitForeach($result, $foreach) { @@ -722,11 +769,10 @@ protected function emitInstanceOf($result, $instanceof) { } protected function emitArguments($result, $arguments) { - $s= sizeof($arguments) - 1; $i= 0; foreach ($arguments as $argument) { + if ($i++) $result->out->write(','); $this->emitOne($result, $argument); - if ($i++ < $s) $result->out->write(', '); } } diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 4c3b2664..0443c6b8 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -32,12 +32,11 @@ public function __construct() { } protected function emitArguments($result, $arguments) { - $s= sizeof($arguments) - 1; $i= 0; foreach ($arguments as $name => $argument) { + if ($i++) $result->out->write(','); if (is_string($name)) $result->out->write($name.':'); $this->emitOne($result, $argument); - if ($i++ < $s) $result->out->write(', '); } } @@ -54,6 +53,11 @@ protected function emitNew($result, $new) { $result->out->write(')'); } + protected function emitThrowExpression($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + } + protected function emitCatch($result, $catch) { $capture= $catch->variable ? ' $'.$catch->variable : ''; if (empty($catch->types)) { @@ -91,13 +95,13 @@ protected function emitMatch($result, $match) { $b++; } $result->out->write('=>'); - $this->emitOne($result, $case->body); + $this->emitAsExpression($result, $case->body); $result->out->write(','); } if ($match->default) { $result->out->write('default=>'); - $this->emitOne($result, $match->default); + $this->emitAsExpression($result, $match->default); } $result->out->write('}'); diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 407b84c2..eeff6928 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -1,5 +1,7 @@ body)) { - $this->rewriteLambda($result, $lambda); + if ($lambda->body instanceof Block) { + $this->enclose($result, $lambda->body, $lambda->signature, function($result, $body) { + $this->emitAll($result, $body->statements); + }); + } else if (is_array($lambda->body)) { // BC for xp-framework/ast <= 7.0 + $this->enclose($result, $lambda, $lambda->signature, function($result, $lambda) { + $this->emitAll($result, $lambda->body); + }); } else { - parent::emitLambda($result, $lambda); + $result->out->write('fn'); + $this->emitSignature($result, $lambda->signature); + $result->out->write('=>'); + $this->emitOne($result, $lambda->body); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index ca691c1e..e9f02be5 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -1,5 +1,7 @@ codegen->search($lambda, 'variable') as $var) { - if (isset($result->locals[$var->name])) { - $capture[$var->name]= true; + $this->enclose($result, $lambda, $lambda->signature, function($result, $lambda) { + if ($lambda->body instanceof Block) { + $this->emitAll($result, $lambda->body->statements); + } else if (is_array($lambda->body)) { // BC for xp-framework/ast <= 7.0 + $this->emitAll($result, $lambda->body); + } else { + $result->out->write('return '); + $this->emitOne($result, $lambda->body); + $result->out->write(';'); } - } - unset($capture['this']); - - $result->stack[]= $result->locals; - $result->locals= []; - - $result->out->write('function'); - $this->emitSignature($result, $lambda->signature); - foreach ($lambda->signature->parameters as $param) { - unset($capture[$param->name]); - } - - if ($capture) { - $result->out->write(' use($'.implode(', $', array_keys($capture)).')'); - foreach ($capture as $name => $_) { - $result->locals[$name]= true; - } - } - - if (is_array($lambda->body)) { - $result->out->write('{'); - $this->emitAll($result, $lambda->body); - $result->out->write('}'); - } else { - $result->out->write('{ return '); - $this->emitOne($result, $lambda->body); - $result->out->write('; }'); - } - - $result->locals= array_pop($result->stack); + }); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 82f0dbed..64663f4c 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -69,7 +69,7 @@ public function run($arg) { } #[Test, Values([[200, 'OK'], [302, 'Redirect'], [404, 'Error #404']])] - public function match_with_multiple($input, $expected) { + public function match_with_multiple_cases($input, $expected) { $r= $this->run('class { public function run($arg) { return match ($arg) { @@ -83,6 +83,27 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test, Values([['PING', '+PONG'], ['MSG', '+OK Re: Test'], ['XFER', '-ERR Unknown XFER']])] + public function match_with_multiple_statements($input, $expected) { + $r= $this->run('class { + public function run($type) { + $value= "Test"; + return match ($type) { + "PING" => "+PONG", + "MSG" => { + $reply= "Re: ".$value; + return "+OK $reply"; + }, + default => { + return "-ERR Unknown ".$type; + } + }; + } + }', $input); + + Assert::equals($expected, $r); + } + #[Test, Values([[0, 'no items'], [1, 'one item'], [5, '5 items'], [10, '10+ items'],])] public function match_with_binary($input, $expected) { $r= $this->run('class { From 7d53d1cd3e41aa03cbad91c99c5fd382120e9e2f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 17:10:01 +0100 Subject: [PATCH 339/926] Ensure parameters get added to locals in enclose() --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a2ee150d..89593541 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -63,6 +63,8 @@ protected function enclose($result, $node, $signature, $emit) { } unset($capture['this']); + $result->stack[]= $result->locals; + $result->locals= []; if ($signature) { $result->out->write('function'); $this->emitSignature($result, $signature); @@ -73,8 +75,6 @@ protected function enclose($result, $node, $signature, $emit) { $result->out->write('function()'); } - $result->stack[]= $result->locals; - $result->locals= []; if ($capture) { $result->out->write('use($'.implode(', $', array_keys($capture)).')'); foreach ($capture as $name => $_) { From d09be04183f3ae7976335a2eaccfc2077e0329dd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 17:59:13 +0100 Subject: [PATCH 340/926] Verify we do not add `$this` to use list Would yield a compile error: Cannot use $this as lexical variable --- .../lang/ast/unittest/emit/ExceptionsTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index cb6c57e8..f55c529d 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -105,6 +105,16 @@ public function run() { } } + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_this() { + $this->run('class { + private $message= "test"; + public function run($user= null) { + return $user ?? throw new \\lang\\IllegalArgumentException($this->message); + } + }'); + } + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_null_coalesce() { $t= $this->type('class { From 624da1edaa83d0f1a7a771267c4314a85c05c39e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 20:42:21 +0100 Subject: [PATCH 341/926] Make compatible with AST library evolution --- composer.json | 2 +- .../RewriteBlockLambdaExpressions.class.php | 4 -- .../emit/RewriteLambdaExpressions.class.php | 2 - .../unittest/emit/AnnotationsTest.class.php | 72 ------------------- 4 files changed, 1 insertion(+), 79 deletions(-) diff --git a/composer.json b/composer.json index 9234c59a..baa75028 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-refactor/block-expressions as 7.0.0", + "xp-framework/ast": "dev-master as 7.0.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index eeff6928..008dd28d 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -15,10 +15,6 @@ protected function emitLambda($result, $lambda) { $this->enclose($result, $lambda->body, $lambda->signature, function($result, $body) { $this->emitAll($result, $body->statements); }); - } else if (is_array($lambda->body)) { // BC for xp-framework/ast <= 7.0 - $this->enclose($result, $lambda, $lambda->signature, function($result, $lambda) { - $this->emitAll($result, $lambda->body); - }); } else { $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index e9f02be5..3e905c63 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -13,8 +13,6 @@ protected function emitLambda($result, $lambda) { $this->enclose($result, $lambda, $lambda->signature, function($result, $lambda) { if ($lambda->body instanceof Block) { $this->emitAll($result, $lambda->body->statements); - } else if (is_array($lambda->body)) { // BC for xp-framework/ast <= 7.0 - $this->emitAll($result, $lambda->body); } else { $result->out->write('return '); $this->emitOne($result, $lambda->body); diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 5f4f0fba..81415cfc 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -113,76 +113,4 @@ public function multiple_member_annotations() { $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } - - #[Test] - public function xp_type_annotation() { - $t= $this->type(' - #[@test] - class { }' - ); - Assert::equals(['test' => null], $t->getAnnotations()); - } - - /** @deprecated */ - #[Test] - public function xp_type_annotation_with_named_pairs() { - $t= $this->type(' - #[@resource(path= "/", authenticated= true)] - class { }' - ); - Assert::equals(['resource' => ['path' => '/', 'authenticated' => true]], $t->getAnnotations()); - \xp::gc(); - } - - #[Test] - public function xp_type_annotations() { - $t= $this->type(' - #[@resource("/"), @authenticated] - class { }' - ); - Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); - } - - #[Test] - public function xp_type_multiline() { - $t= $this->type(' - #[@verify(function($arg) {return $arg;})] - class { }' - ); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f('test')); - } - - #[Test] - public function xp_method_annotations() { - $t= $this->type(' - class { - - #[@test] - public function succeeds() { } - - #[@test, @expect(\lang\IllegalArgumentException::class)] - public function fails() { } - - #[@test, @values([[1, 2, 3],])] - public function cases() { } - }' - ); - Assert::equals(['test' => null], $t->getMethod('succeeds')->getAnnotations()); - Assert::equals(['test' => null, 'expect' => IllegalArgumentException::class], $t->getMethod('fails')->getAnnotations()); - Assert::equals(['test' => null, 'values' => [[1, 2, 3]]], $t->getMethod('cases')->getAnnotations()); - } - - #[Test] - public function xp_param_annotation() { - $t= $this->type(' - class { - - #[@test, @$arg: inject("conn")] - public function fixture($arg) { } - }' - ); - Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); - Assert::equals(['inject' => 'conn'], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); - } } \ No newline at end of file From c5b72ea29c5f47e1029341ffa6553b2901059a35 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 20:46:27 +0100 Subject: [PATCH 342/926] QA: Fix annotation casing [skip ci] --- src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 81415cfc..befdf164 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -94,7 +94,7 @@ public function param() { #[Test] public function params() { - $t= $this->type('class { public function fixture(#[inject(["name" => "a"])] $a, #[inject] $b) { } }'); + $t= $this->type('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); $m=$t->getMethod('fixture'); Assert::equals( [['inject' => ['name' => 'a']], ['inject' => null]], @@ -104,7 +104,7 @@ public function params() { #[Test] public function multiple_class_annotations() { - $t= $this->type('#[Resource("/"), authenticated] class { }'); + $t= $this->type('#[Resource("/"), Authenticated] class { }'); Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); } From 2143effcace5737445ce8a5edc751a2e61589973 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 20:47:45 +0100 Subject: [PATCH 343/926] Skip tests if commit message contains `[skip ci]` --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56527fb0..e68daadb 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: jobs: tests: + if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.php-versions == '8.1' }} From 16daa81d9adf5c37573281b4c5cc73c573a45c77 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 20:58:46 +0100 Subject: [PATCH 344/926] Document blocks support in match expressions --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 8c3f190c..2d02bcee 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #96: Enclose blocks where PHP only allows expressions. This + not only allows `fn() => { ... }` but also using blocks in `match`. + (@thekid) + ## 5.7.0 / 2020-11-26 * Verified full PHP 8 support now that PHP 8.0.0 has been released, From 18c7233c45025ac4fa54e1eb469de34e378511e3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:00:10 +0100 Subject: [PATCH 345/926] Implement last step of #86 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 2d02bcee..91b13a63 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.0.0 / ????-??-?? + +* Removed support for legacy XP and Hack language annotations, see #86 + (@thekid) * Merged PR #96: Enclose blocks where PHP only allows expressions. This not only allows `fn() => { ... }` but also using blocks in `match`. (@thekid) From 1d742983f12f61c2599599e0258da40d202312d3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:01:45 +0100 Subject: [PATCH 346/926] Document match without expression --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 91b13a63..5f37fd1c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 6.0.0 / ????-??-?? +* Merged PR #93: Allow match without expression: `match { ... }`. See + https://wiki.php.net/rfc/match_expression_v2#allow_dropping_true + (@thekid) * Removed support for legacy XP and Hack language annotations, see #86 (@thekid) * Merged PR #96: Enclose blocks where PHP only allows expressions. This From ff82c48ce88267d97f9e04b596dc4e8317f7e46f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:03:19 +0100 Subject: [PATCH 347/926] Document octal integer literal notation --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 5f37fd1c..6801e72c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 6.0.0 / ????-??-?? +* Merged PR #92: Add support for explicit octal integer literal notation + See https://wiki.php.net/rfc/explicit_octal_notation (PHP 8.1) + (@thekid) * Merged PR #93: Allow match without expression: `match { ... }`. See https://wiki.php.net/rfc/match_expression_v2#allow_dropping_true (@thekid) From 2da748827ca85863cb66a64d33abda4a72092c04 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:08:59 +0100 Subject: [PATCH 348/926] Include our first PHP 8.1 feature --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e1e3eea..8054355b 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses PHP 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses PHP 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php Date: Sat, 28 Nov 2020 21:34:11 +0100 Subject: [PATCH 349/926] Add note on removed support for using curly braces as offset This is inside the AST library, but users should note this here directly as well. See https://wiki.php.net/rfc/deprecate_curly_braces_array_access --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6801e72c..868d9327 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,8 @@ XP Compiler ChangeLog ## 6.0.0 / ????-??-?? +* Removed support for using curly braces as offset (e.g. `$value{0}`) + (@thekid) * Merged PR #92: Add support for explicit octal integer literal notation See https://wiki.php.net/rfc/explicit_octal_notation (PHP 8.1) (@thekid) From 230bc3f0d550bddab4ed960116fafc1fa8e6ef74 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:47:14 +0100 Subject: [PATCH 350/926] Add `-q` command line option --- ChangeLog.md | 3 ++ .../php/xp/compiler/CompileRunner.class.php | 39 +++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 868d9327..73c24151 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 6.0.0 / ????-??-?? +* Added `-q` command line option which suppresses all diagnostic output + from the compiler except for errors + (@thekid) * Removed support for using curly braces as offset (e.g. `$value{0}`) (@thekid) * Merged PR #92: Add support for explicit octal integer literal notation diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index d872bfab..0cb4f715 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -36,6 +36,7 @@ * ``` * * The *-o* and *-n* options accept multiple input sources following them. + * The *-q* option suppresses all diagnostic output except for errors. * * @see https://github.com/xp-framework/rfc/issues/299 */ @@ -47,9 +48,12 @@ public static function main(array $args) { $target= 'PHP.'.PHP_VERSION; $in= $out= '-'; + $quiet= false; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { $target= $args[++$i]; + } else if ('-q' === $args[$i]) { + $quiet= true; } else if ('-o' === $args[$i]) { $out= $args[++$i]; $in= array_slice($args, $i + 1); @@ -86,7 +90,7 @@ public static function main(array $args) { $emit->emitAll(new Result($target), $parse->stream()); $t->stop(); - Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); + $quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); } catch (Errors $e) { $t->stop(); Console::$err->writeLinef('! %s: %s ', $file, $e->diagnostics(' ')); @@ -99,22 +103,25 @@ public static function main(array $args) { } } + if (!$quiet) { + Console::$err->writeLine(); + Console::$err->writeLinef( + "%s Compiled %d file(s) to %s using %s, %d error(s) occurred\033[0m", + $errors ? "\033[41;1;37m×" : "\033[42;1;37m♥", + $total, + $out, + typeof($emit)->getName(), + $errors + ); + Console::$err->writeLinef( + "Memory used: %.2f kB (%.2f kB peak)\nTime taken: %.3f seconds", + Runtime::getInstance()->memoryUsage() / 1024, + Runtime::getInstance()->peakMemoryUsage() / 1024, + $time + ); + } + $output->close(); - Console::$err->writeLine(); - Console::$err->writeLinef( - "%s Compiled %d file(s) to %s using %s, %d error(s) occurred\033[0m", - $errors ? "\033[41;1;37m×" : "\033[42;1;37m♥", - $total, - $out, - typeof($emit)->getName(), - $errors - ); - Console::$err->writeLinef( - "Memory used: %.2f kB (%.2f kB peak)\nTime taken: %.3f seconds", - Runtime::getInstance()->memoryUsage() / 1024, - Runtime::getInstance()->peakMemoryUsage() / 1024, - $time - ); return $errors ? 1 : 0; } } \ No newline at end of file From 9855d2d91f1dd5b569eb85f2a482c6d6f5e21cc2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:50:44 +0100 Subject: [PATCH 351/926] Align README and compiler help message --- README.md | 4 ++++ src/main/php/xp/compiler/CompileRunner.class.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8054355b..43f44fb8 100755 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ $ echo " Date: Sat, 28 Nov 2020 21:51:26 +0100 Subject: [PATCH 352/926] Remove transformation API --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 43f44fb8..687ff170 100755 --- a/README.md +++ b/README.md @@ -80,9 +80,6 @@ $ composer require xp-lang/php-is-operator $ xp compile Usage: xp compile [] -@FileSystemCL<./vendor/xp-framework/ast/src/main/php -lang.ast.syntax.TransformationApi - @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> lang.ast.syntax.php.Using From d092852eb7a72d5b1ee6ced3b498e06f35ebc9bb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 21:53:50 +0100 Subject: [PATCH 353/926] Mention Hack language for `array` and `using` [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 687ff170..fbd8cc17 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses PHP 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack language, PHP 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php Date: Sat, 28 Nov 2020 23:12:31 +0100 Subject: [PATCH 354/926] Remove emitStart() - start tokens are no longer produced --- src/main/php/lang/ast/emit/PHP.class.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index efaefaa5..b9accf75 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -108,10 +108,6 @@ protected function emitAsExpression($result, $expression) { } } - protected function emitStart($result, $start) { - // NOOP - } - protected function emitNamespace($result, $declaration) { $result->out->write('namespace '.$declaration->name); } From 6dca8ed7bf1ba4542f71bb1f61c189801233e377 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 23:13:12 +0100 Subject: [PATCH 355/926] Remove emitOperator() and emitName() which are no longer used --- src/main/php/lang/ast/emit/PHP.class.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b9accf75..08db59eb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -118,14 +118,6 @@ protected function emitImport($result, $import) { } } - protected function emitOperator($result, $operator) { - // NOOP - } - - protected function emitName($result, $name) { - // NOOP - } - protected function emitCode($result, $code) { $result->out->write($code->value); } From 005722993daa855469087eda7f162314f00a6eb7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 23:27:27 +0100 Subject: [PATCH 356/926] Use xp-framework/ast version 7.0.0 now that is has been released --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index baa75028..66cf5680 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-master as 7.0.0", + "xp-framework/ast": "^7.0", "php" : ">=7.0.0" }, "require-dev" : { From abffd44f02d5e213f35b3d54f9dea3dee9acd7fc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 28 Nov 2020 23:28:17 +0100 Subject: [PATCH 357/926] Release 6.0.0 --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 73c24151..b81116be 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 6.0.0 / ????-??-?? +## 6.0.0 / 2020-11-28 * Added `-q` command line option which suppresses all diagnostic output from the compiler except for errors From bfde3484d44c373d9cefdc34e26bbee60ad0738f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 00:51:08 +0100 Subject: [PATCH 358/926] Remove unused method Result::buffer() --- src/main/php/lang/ast/Result.class.php | 21 ------------------- .../lang/ast/unittest/ResultTest.class.php | 11 ---------- 2 files changed, 32 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 3c18e12a..bc89689a 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,7 +1,5 @@ codegen->symbol(); } - - /** - * Collects emitted code into a buffer and returns it - * - * @param function(lang.ast.Result): void $callable - * @return string - */ - public function buffer($callable) { - $out= $this->out; - $buffer= new MemoryOutputStream(); - $this->out= new StringWriter($buffer); - - try { - $callable($this); - return $buffer->getBytes(); - } finally { - $this->out= $out; - } - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index bd25c377..dba2b6c3 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -30,15 +30,4 @@ public function writes_preamble($preamble) { $r= new Result(new StringWriter($out), $preamble); Assert::equals($preamble, $out->bytes()); } - - #[Test] - public function writing_via_buffer_does_not_affect_underlying_output() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); - $r->buffer(function($r) { - $r->out->write('namespace test;'); - $r->out->write('use unittest\Assert;'); - }); - Assert::equals('bytes()); - } } \ No newline at end of file From e46646747ee9edffad87bb8774bb10ba45e06b5c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 00:53:46 +0100 Subject: [PATCH 359/926] Remove unused member $call --- src/main/php/lang/ast/Result.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index bc89689a..1e354b56 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -7,7 +7,6 @@ class Result { public $meta= []; public $locals= []; public $stack= []; - public $call= []; /** * Starts an result stream, including a preamble From 2f6f7b61683dc36d50f3580928c9fc43971ba6fd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 00:53:56 +0100 Subject: [PATCH 360/926] Add test for writing to result --- src/test/php/lang/ast/unittest/ResultTest.class.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index dba2b6c3..e5e0c113 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -18,7 +18,7 @@ public function creates_unique_temporary_variables() { } #[Test] - public function writes_php_open_tag_as_default_preable() { + public function writes_php_open_tag_as_default_preamble() { $out= new MemoryOutputStream(); $r= new Result(new StringWriter($out)); Assert::equals('bytes()); @@ -30,4 +30,12 @@ public function writes_preamble($preamble) { $r= new Result(new StringWriter($out), $preamble); Assert::equals($preamble, $out->bytes()); } + + #[Test] + public function write() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out)); + $r->out->write('echo "Hello";'); + Assert::equals('bytes()); + } } \ No newline at end of file From ba9783e59fbdab4c1e3e5079e163ef61e2bfd4f1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 00:54:39 +0100 Subject: [PATCH 361/926] QA: Apidoc [skip ci] --- src/main/php/lang/ast/Result.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 1e354b56..166d192c 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -9,9 +9,9 @@ class Result { public $stack= []; /** - * Starts an result stream, including a preamble + * Starts a result stream, including a preamble * - * @param io.streams.Writer + * @param io.streams.Writer $out * @param string $preamble */ public function __construct($out, $preamble= ' Date: Sun, 29 Nov 2020 02:38:00 +0100 Subject: [PATCH 362/926] Include emitters in `xp compile` output --- README.md | 11 +++-- .../php/xp/compiler/CompileRunner.class.php | 4 +- src/main/php/xp/compiler/Usage.class.php | 40 +++++++++++-------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index fbd8cc17..120274a7 100755 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ $ xp compile -o dist.xar src/main/php/ # Compile src/main/php, do not write output $ xp compile -n src/main/php/ -# Target PHP 7.0 (default target is current PHP version) -$ xp compile -t PHP.7.0 HelloWorld.php HelloWorld.class.php +# Target PHP 7.4 (default target is current PHP version) +$ xp compile -t PHP.7.4 HelloWorld.php HelloWorld.class.php ``` The -o and -n options accept multiple input sources following them. @@ -81,7 +81,12 @@ $ xp compile Usage: xp compile [] @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> -lang.ast.syntax.php.Using +lang.ast.emit.PHP70 +lang.ast.emit.PHP71 +lang.ast.emit.PHP72 +lang.ast.emit.PHP74 +lang.ast.emit.PHP80 [*] +lang.ast.syntax.php.Using [*] @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> lang.ast.syntax.php.IsOperator diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 2b45cfb9..8652b82c 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -30,9 +30,9 @@ * ```sh * $ xp compile -n src/main/php/ * ``` - * - Target PHP 7.0 (default target is current PHP version) + * - Target PHP 7.4 (default target is current PHP version) * ```sh - * $ xp compile -t PHP.7.0 HelloWorld.php HelloWorld.class.php + * $ xp compile -t PHP.7.4 HelloWorld.php HelloWorld.class.php * ``` * * The *-o* and *-n* options accept multiple input sources following them. diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 5326f435..d17af212 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,6 +1,7 @@ writeLine('Usage: xp compile []'); - // Show syntax implementations sorted by class loader - $loaders= $sorted= []; - foreach (Language::named('PHP')->extensions() as $extension) { - $t= typeof($extension); - $l= $t->getClassLoader(); - $hash= $l->hashCode(); - if (isset($sorted[$hash])) { - $sorted[$hash][]= $t; - } else { - $loaders[$hash]= $l; - $sorted[$hash]= [$t]; + $impl= new class() { + public $byLoader= []; + + public function add($t, $active= false) { + $this->byLoader[$t->getClassLoader()->toString()][$t->getName()]= $active; + } + }; + + $default= Emitter::forRuntime('PHP.'.PHP_VERSION); + foreach (Package::forName('lang.ast.emit')->getClasses() as $class) { + if ($class->isSubclassOf(Emitter::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { + $impl->add($class, $class->equals($default)); } } - foreach ($sorted as $hash => $list) { + + foreach (Language::named('PHP')->extensions() as $extension) { + $impl->add(typeof($extension), true); + } + + // Show implementations sorted by class loader + foreach ($impl->byLoader as $loader => $list) { Console::$err->writeLine(); - Console::$err->writeLine("\033[33m@", $loaders[$hash], "\033[0m"); - foreach ($list as $syntax) { - Console::$err->writeLine($syntax->getName()); + Console::$err->writeLine("\033[33m@", $loader, "\033[0m"); + foreach ($list as $impl => $active) { + Console::$err->writeLine($impl, $active ? " [\033[36m*\033[0m]" : ''); } } return 2; From 6435d06233c8d2a57c7e60a6c727ee0523b796cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 02:41:24 +0100 Subject: [PATCH 363/926] Document new feature in `xp compile` output [skip ci] --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b81116be..a529c10d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Included emitters in `xp compile` output - @thekid + ## 6.0.0 / 2020-11-28 * Added `-q` command line option which suppresses all diagnostic output From 019aea6859d28fc5a8f57feabedf638b9354d2da Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 19:14:27 +0100 Subject: [PATCH 364/926] Include languages in `xp compile` output --- src/main/php/xp/compiler/Usage.class.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index d17af212..f52c94b6 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -5,6 +5,7 @@ use util\cmd\Console; class Usage { + const RUNTIME = 'PHP'; /** @return int */ public static function main(array $args) { @@ -18,15 +19,21 @@ public function add($t, $active= false) { } }; - $default= Emitter::forRuntime('PHP.'.PHP_VERSION); + $emitter= Emitter::forRuntime(self::RUNTIME.'.'.PHP_VERSION); foreach (Package::forName('lang.ast.emit')->getClasses() as $class) { if ($class->isSubclassOf(Emitter::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { - $impl->add($class, $class->equals($default)); + $impl->add($class, $class->equals($emitter)); } } - foreach (Language::named('PHP')->extensions() as $extension) { - $impl->add(typeof($extension), true); + foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { + if ($class->isSubclassOf(Language::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { + $impl->add($class, self::RUNTIME === $class->getSimpleName()); + } + } + + foreach (Language::named(self::RUNTIME)->extensions() as $extension) { + $impl->add(typeof($extension), 'true'); } // Show implementations sorted by class loader From 1dfec50b57fb60e82a93954d7bc0acd7a420cd41 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 19:17:53 +0100 Subject: [PATCH 365/926] Use better comparison --- src/main/php/xp/compiler/Usage.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index f52c94b6..e0b1ea06 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -26,13 +26,14 @@ public function add($t, $active= false) { } } + $language= Language::named(self::RUNTIME); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { if ($class->isSubclassOf(Language::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { - $impl->add($class, self::RUNTIME === $class->getSimpleName()); + $impl->add($class, $class->isInstance($language)); } } - foreach (Language::named(self::RUNTIME)->extensions() as $extension) { + foreach ($language->extensions() as $extension) { $impl->add(typeof($extension), 'true'); } From fa1a94bd57320004a09bcedc2c4611e6f713ce99 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 20:41:12 +0100 Subject: [PATCH 366/926] Document languages are shown in `xp compile` output [skip ci] --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index a529c10d..d01ad154 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -* Included emitters in `xp compile` output - @thekid +* Included languages and emitters in `xp compile` output - @thekid ## 6.0.0 / 2020-11-28 From 7b7412f5bd1dbcf5350c5b62ec3b5d4b849e6dba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Nov 2020 23:29:18 +0100 Subject: [PATCH 367/926] QA: WS / coding standards adjustments --- .../php/lang/ast/unittest/emit/AnnotationsTest.class.php | 2 +- .../lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 3 +-- .../lang/ast/unittest/emit/ArgumentUnpackingTest.class.php | 1 - .../php/lang/ast/unittest/emit/ArrayTypesTest.class.php | 1 + src/test/php/lang/ast/unittest/emit/CastingTest.class.php | 6 +++--- .../php/lang/ast/unittest/emit/MultipleCatchTest.class.php | 4 ++-- .../php/lang/ast/unittest/emit/PrecedenceTest.class.php | 6 +++--- src/test/php/lang/ast/unittest/emit/UsingTest.class.php | 1 + src/test/php/lang/ast/unittest/emit/YieldTest.class.php | 1 + 9 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index befdf164..f42d167b 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -95,7 +95,7 @@ public function param() { #[Test] public function params() { $t= $this->type('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); - $m=$t->getMethod('fixture'); + $m= $t->getMethod('fixture'); Assert::equals( [['inject' => ['name' => 'a']], ['inject' => null]], [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index c81a6722..96c616da 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -75,8 +75,7 @@ public function run() { #[Test] public function type_information() { $t= $this->type('class { - public function __construct(private int $id, private string $name) { - } + public function __construct(private int $id, private string $name) { } }'); Assert::equals( [Primitive::$INT, Primitive::$STRING], diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index 9cda4a18..cbf78e8c 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -1,6 +1,5 @@ run( 'class { @@ -20,7 +20,7 @@ public function run($value) { )); } - #[Test, Values(["0", "1", "-1", "6100", "", 0.5, -1.5, 0, 1, -1, true, false])] + #[Test, Values(['0', '1', '-1', '6100', '', 0.5, -1.5, 0, 1, -1, true, false])] public function int_cast($value) { Assert::equals((int)$value, $this->run( 'class { @@ -32,7 +32,7 @@ public function run($value) { )); } - #[Test, Values([[[]], [[0, 1, 2]], [['key' => 'value']], null, false, true, 1, 1.5, "", "test"])] + #[Test, Values([[[]], [[0, 1, 2]], [['key' => 'value']], null, false, true, 1, 1.5, '', 'test'])] public function array_cast($value) { Assert::equals((array)$value, $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php index 73f34529..76881fc9 100755 --- a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php @@ -17,11 +17,11 @@ public function run($t) { try { throw new $t("test"); } catch (\\lang\\IllegalArgumentException | \\lang\\IllegalStateException $e) { - return "caught ".get_class($e); + return "Caught ".get_class($e); } } }'); - Assert::equals('caught '.$type, $t->newInstance()->run($type)); + Assert::equals('Caught '.$type, $t->newInstance()->run($type)); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index 06326114..b89c1c72 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -4,7 +4,7 @@ class PrecedenceTest extends EmittingTest { - #[Test, Values([['2 + 3 * 4', 14], ['2 + 8 / 4', 4], ['2 + 3 ** 2', 11], ['2 + 5 % 2', 3],])] + #[Test, Values([['2 + 3 * 4', 14], ['2 + 8 / 4', 4], ['2 + 3 ** 2', 11], ['2 + 5 % 2', 3]])] public function mathematical($input, $result) { Assert::equals($result, $this->run( 'class { @@ -24,7 +24,7 @@ public function run() { } }' ); - Assert::equals('('.$t->getName().')', $t->newInstance()->run()); + Assert::equals('('.$t->literal().')', $t->newInstance()->run()); } #[Test] @@ -38,6 +38,6 @@ public function run() { } }' ); - Assert::equals(2, $t->newinstance()->run()); + Assert::equals(2, $t->newInstance()->run()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index 47700ca7..0dc7e0d5 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -1,6 +1,7 @@ Date: Mon, 4 Jan 2021 13:37:58 +0100 Subject: [PATCH 368/926] Prepare 6.1.0 --- ChangeLog.md | 2 ++ .../php/lang/ast/unittest/emit/BracesTest.class.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d01ad154..4abc0ac0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.1.0 / 2021-01-04 + * Included languages and emitters in `xp compile` output - @thekid ## 6.0.0 / 2020-11-28 diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index 930ad5aa..db77a66f 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -93,6 +93,18 @@ public function run() { Assert::equals($expected, $r); } + #[Test] + public function function_call_in_braces() { + $r= $this->run('class { + public function run() { + $e= STDOUT; + return false !== (fstat(STDOUT)); + } + }'); + + Assert::true($r); + } + #[Test] public function invoke_on_braced_null_coalesce() { $r= $this->run('class { From 7b2686f5b4ba4c8710371520029d3608fdcf798a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 19:36:55 +0100 Subject: [PATCH 369/926] Check XP core for nullable support and change assertions Fixes issue #103 --- ChangeLog.md | 2 ++ .../ast/unittest/emit/NullableSupport.class.php | 13 +++++++++++++ .../lang/ast/unittest/emit/ParameterTest.class.php | 11 ++++++----- 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/NullableSupport.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 4abc0ac0..9088d2e3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed issue #103: Nullable types - @thekid + ## 6.1.0 / 2021-01-04 * Included languages and emitters in `xp compile` output - @thekid diff --git a/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php b/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php new file mode 100755 index 00000000..186ed02a --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php @@ -0,0 +1,13 @@ +param('Value $param= null')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('Value $param= null')->getType()); } #[Test] public function nullable_value_type() { - Assert::equals(new XPClass(Value::class), $this->param('?Value $param')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param')->getType()); } #[Test] @@ -64,17 +65,17 @@ public function string_typed() { #[Test] public function string_typed_with_null() { - Assert::equals(Primitive::$STRING, $this->param('string $param= null')->getType()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('string $param= null')->getType()); } #[Test] public function nullable_string_type() { - Assert::equals(Primitive::$STRING, $this->param('?string $param')->getType()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getType()); } #[Test, Action(eval: 'new RuntimeVersion(">=7.1")')] public function nullable_string_type_restriction() { - Assert::equals(Primitive::$STRING, $this->param('?string $param')->getTypeRestriction()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getTypeRestriction()); } #[Test] From a9995d074422b09119af5549b74847e710b420da Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 19:39:25 +0100 Subject: [PATCH 370/926] Prevent strlen() complaining about NULL Fixes issue #102, PHP 8.1 compatiblity --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 08db59eb..a95d8c07 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -358,7 +358,7 @@ protected function attributes($result, $annotations, $target) { /** Removes leading, intermediate and trailing stars from apidoc comments */ private function comment($comment) { - if (0 === strlen($comment)) { + if (null === $comment || '' === $comment) { return 'null'; } else if ('/' === $comment[0]) { return "'".str_replace("'", "\\'", trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 3, -2))))."'"; From 33d1e9c4ebcd82fea3cf43085b1d263b9fb60f9e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 19:40:57 +0100 Subject: [PATCH 371/926] Fix PHP 8.1 compatibility --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index 9088d2e3..28598e50 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed issue #102: PHP 8.1 compatiblity - @thekid * Fixed issue #103: Nullable types - @thekid ## 6.1.0 / 2021-01-04 From fae72a856c0a88592298fdcb56002cf25a6fddd1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 20:03:52 +0100 Subject: [PATCH 372/926] Add tests for issue #101 --- .../emit/ControlStructuresTest.class.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index ac177871..e803f0f2 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -162,4 +162,35 @@ public function run($arg) { } }', SEEK_END); } + + #[Test] + public function match_without_arg_inside_fn() { + $r= $this->run('class { + public function run() { + return fn($arg) => match { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }'); + + Assert::equals('10+ items', $r(10)); + } + + #[Test] + public function match_with_arg_inside_fn() { + $r= $this->run('class { + public function run() { + return fn($arg) => match ($arg) { + 0 => "no items", + 1 => "one item", + default => $arg." items", + }; + } + }'); + + Assert::equals('one item', $r(1)); + } } \ No newline at end of file From 77f989a5de868fc7272bf0ebaa3f4b7ba840b1de Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 20:12:18 +0100 Subject: [PATCH 373/926] Prepare 6.1.1 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 28598e50..9766cad3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.1.1 / 2021-01-04 + +* Fixed issue #102: Call to a member function children()... - @thekid * Fixed issue #102: PHP 8.1 compatiblity - @thekid * Fixed issue #103: Nullable types - @thekid From 3a0d193d55ee01be8202858fecb5d4674aae1f62 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 22 Feb 2021 20:13:04 +0100 Subject: [PATCH 374/926] Fix typo --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9766cad3..b199b15a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,7 @@ XP Compiler ChangeLog ## 6.1.1 / 2021-01-04 * Fixed issue #102: Call to a member function children()... - @thekid -* Fixed issue #102: PHP 8.1 compatiblity - @thekid +* Fixed issue #102: PHP 8.1 compatibility - @thekid * Fixed issue #103: Nullable types - @thekid ## 6.1.0 / 2021-01-04 From f7c5e3f13d52104d697150e5bd65fe9d6f49497e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 11:33:52 +0100 Subject: [PATCH 375/926] Support arbitrary expressions in property initializations and parameter defaults See https://wiki.php.net/rfc/new_in_initializers --- src/main/php/lang/ast/emit/PHP.class.php | 100 +++++++++++++++--- .../lang/ast/unittest/emit/Handle.class.php | 4 + .../emit/InitializeWithNewTest.class.php | 84 +++++++++++++++ 3 files changed, 175 insertions(+), 13 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a95d8c07..285f75ef 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,6 +1,7 @@ values as $node) { + if (!$this->staticScalar($node)) return false; + } + return true; + } else if ($node instanceof ScopeExpression) { + return $node->member instanceof Literal; + } + return false; + } + /** * As of PHP 7.4: Property type declarations support all type declarations * supported by PHP with the exception of void and callable. @@ -88,6 +114,21 @@ protected function enclose($result, $node, $signature, $emit) { $result->locals= array_pop($result->stack); } + /** + * Emits local initializations + * + * @param lang.ast.Result $result + * @param [:lang.ast.Node] $init + * @return void + */ + protected function emitInitializations($result, $init) { + foreach ($init as $assign => $expression) { + $result->out->write($assign.'='); + $this->emitOne($result, $expression); + $result->out->write(';'); + } + } + /** * Convert blocks to IIFEs to allow a list of statements where PHP syntactically * doesn't, e.g. `fn`-style lambdas or match expressions. @@ -235,8 +276,13 @@ protected function emitParameter($result, $parameter) { $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); } if ($parameter->default) { - $result->out->write('='); - $this->emitOne($result, $parameter->default); + if ($this->staticScalar($parameter->default)) { + $result->out->write('='); + $this->emitOne($result, $parameter->default); + } else { + $result->out->write('=null'); + $result->locals['$']['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; + } } $result->locals[$parameter->name]= true; } @@ -297,6 +343,7 @@ protected function emitLambda($result, $lambda) { protected function emitClass($result, $class) { array_unshift($result->meta, []); + $result->locals= ['$' => [], '@' => []]; $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); $class->parent && $result->out->write(' extends '.$class->parent); @@ -306,7 +353,16 @@ protected function emitClass($result, $class) { $this->emitOne($result, $member); } + // Create constructor for property initializations to non-static scalars + // which have not already been emitted inside constructor + if ($result->locals['$']) { + $result->out->write('public function __construct() {'); + $this->emitInitializations($result, $result->locals['$']); + $result->out->write('}'); + } + $result->out->write('static function __init() {'); + $this->emitInitializations($result, $result->locals['@']); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name.'::__init();'); } @@ -452,15 +508,31 @@ protected function emitProperty($result, $property) { $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); if (isset($property->expression)) { - $result->out->write('='); - $this->emitOne($result, $property->expression); + if ($this->staticScalar($property->expression)) { + $result->out->write('='); + $this->emitOne($result, $property->expression); + } else if (in_array('static', $property->modifiers)) { + $result->locals['@']['self::$'.$property->name]= $property->expression; + } else { + $result->locals['$']['$this->'.$property->name]= $property->expression; + } } $result->out->write(';'); } protected function emitMethod($result, $method) { + + // Include non-static initializations for members in constructor, removing + // them to signal emitClass() no constructor needs to be generated. + if ('__construct' === $method->name && isset($result->locals['$'])) { + $locals= ['this' => true, '$' => $result->locals['$']]; + $result->locals['$']= []; + } else { + $locals= ['this' => true, '$' => []]; + } $result->stack[]= $result->locals; - $result->locals= ['this' => true]; + $result->locals= $locals; + $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', DETAIL_ANNOTATIONS => $method->annotations, @@ -469,11 +541,14 @@ protected function emitMethod($result, $method) { DETAIL_ARGUMENTS => [] ]; - $declare= $promote= $params= ''; + $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); + $this->emitSignature($result, $method->signature); + + $promoted= ''; foreach ($method->signature->parameters as $param) { if (isset($param->promote)) { - $declare.= $param->promote.' $'.$param->name.';'; - $promote.= '$this->'.$param->name.'= '.($param->reference ? '&$' : '$').$param->name.';'; + $promoted.= $param->promote.' $'.$param->name.';'; + $result->locals['$']['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); $result->meta[0][self::PROPERTY][$param->name]= [ DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', DETAIL_ANNOTATIONS => [], @@ -485,18 +560,17 @@ protected function emitMethod($result, $method) { $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } - $result->out->write($declare); - $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $this->emitSignature($result, $method->signature); if (null === $method->body) { $result->out->write(';'); } else { - $result->out->write(' {'.$promote); + $result->out->write(' {'); + $this->emitInitializations($result, $result->locals['$']); $this->emitAll($result, $method->body); $result->out->write('}'); } + $result->out->write($promoted); $result->meta[0][self::METHOD][$method->name]= $meta; $result->locals= array_pop($result->stack); } diff --git a/src/test/php/lang/ast/unittest/emit/Handle.class.php b/src/test/php/lang/ast/unittest/emit/Handle.class.php index 964f3c04..73be3d00 100755 --- a/src/test/php/lang/ast/unittest/emit/Handle.class.php +++ b/src/test/php/lang/ast/unittest/emit/Handle.class.php @@ -9,6 +9,10 @@ class Handle implements \IDisposable { public function __construct($id) { $this->id= $id; } + public function redirect($id) { + $this->id= $id; + } + public function read($bytes= 8192) { self::$called[]= 'read@'.$this->id; diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php new file mode 100755 index 00000000..752041b8 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php @@ -0,0 +1,84 @@ +run('use lang\ast\unittest\emit\Handle; class { + private $h= new Handle(0); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function property_initialization_accessible_inside_constructor() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private $h= new Handle(0); + + public function __construct() { + $this->h->redirect(1); + } + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r); + } + + #[Test] + public function promoted_property() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function __construct(private $h= new Handle(0)) { } + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function parameter_default_when_omitted() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run($h= new Handle(0)) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function parameter_default_when_passed() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run($h= new Handle(0)) { + return $h; + } + }', new Handle(1)); + Assert::equals(new Handle(1), $r); + } + + #[Test] + public function property_reference_as_parameter_default() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private static $h= new Handle(0); + + public function run($h= self::$h) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } +} \ No newline at end of file From 48cea29051d582ff6ab5d8bbbe8ba9bfa5a7d976 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 11:44:30 +0100 Subject: [PATCH 376/926] Verify parameter and property type restrictions work --- .../emit/InitializeWithNewTest.class.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php index 752041b8..2eb575f7 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php @@ -81,4 +81,26 @@ public function run($h= self::$h) { }'); Assert::equals(new Handle(0), $r); } + + #[Test] + public function typed_proprety() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private Handle $h= new Handle(0); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function typed_parameter() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run(Handle $h= new Handle(0)) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } } \ No newline at end of file From 06c161db136fb6f00f328461f519a7f9ad1c26e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 11:49:01 +0100 Subject: [PATCH 377/926] Add support for initializing static variables with arbitrary expressions --- src/main/php/lang/ast/emit/PHP.class.php | 9 +++++++-- .../unittest/emit/InitializeWithNewTest.class.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 285f75ef..09106eb8 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -185,8 +185,13 @@ protected function emitStatic($result, $static) { foreach ($static->initializations as $variable => $initial) { $result->out->write('static $'.$variable); if ($initial) { - $result->out->write('='); - $this->emitOne($result, $initial); + if ($this->staticScalar($initial)) { + $result->out->write('='); + $this->emitOne($result, $initial); + } else { + $result->out->write('= null; null === $'.$variable.' && $'.$variable.'= '); + $this->emitOne($result, $initial); + } } $result->out->write(';'); } diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php index 2eb575f7..0b52483d 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php @@ -103,4 +103,16 @@ public function run(Handle $h= new Handle(0)) { }'); Assert::equals(new Handle(0), $r); } + + #[Test] + public function static_variable() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run() { + static $h= new Handle(0); + + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } } \ No newline at end of file From 8f2c8fc080c3029cf522575fe19dd1ef1cf06893 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 11:51:24 +0100 Subject: [PATCH 378/926] Rename staticScalar() -> isConstant() --- src/main/php/lang/ast/emit/PHP.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 09106eb8..0ef4aeef 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -32,7 +32,7 @@ protected function declaration($name) { } /** - * Returns whether a given node is a so-called "static scalar" expression: + * Returns whether a given node is a constant expression: * * - Any literal * - Arrays where all members are literals @@ -42,12 +42,12 @@ protected function declaration($name) { * @param lang.ast.Node $node * @return bool */ - protected function staticScalar($node) { + protected function isConstant($node) { if ($node instanceof Literal) { return true; } else if ($node instanceof ArrayLiteral) { foreach ($node->values as $node) { - if (!$this->staticScalar($node)) return false; + if (!$this->isConstant($node)) return false; } return true; } else if ($node instanceof ScopeExpression) { @@ -185,7 +185,7 @@ protected function emitStatic($result, $static) { foreach ($static->initializations as $variable => $initial) { $result->out->write('static $'.$variable); if ($initial) { - if ($this->staticScalar($initial)) { + if ($this->isConstant($initial)) { $result->out->write('='); $this->emitOne($result, $initial); } else { @@ -281,7 +281,7 @@ protected function emitParameter($result, $parameter) { $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); } if ($parameter->default) { - if ($this->staticScalar($parameter->default)) { + if ($this->isConstant($parameter->default)) { $result->out->write('='); $this->emitOne($result, $parameter->default); } else { @@ -513,7 +513,7 @@ protected function emitProperty($result, $property) { $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); if (isset($property->expression)) { - if ($this->staticScalar($property->expression)) { + if ($this->isConstant($property->expression)) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { From c4b14a49017d9b9506acf638288bf8dc25b6d6d4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:07:18 +0100 Subject: [PATCH 379/926] Add tests for more expressions used as property initializer --- .../lang/ast/unittest/emit/Handle.class.php | 5 ++++ ...> InitializeWithExpressionsTest.class.php} | 28 +++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) rename src/test/php/lang/ast/unittest/emit/{InitializeWithNewTest.class.php => InitializeWithExpressionsTest.class.php} (76%) diff --git a/src/test/php/lang/ast/unittest/emit/Handle.class.php b/src/test/php/lang/ast/unittest/emit/Handle.class.php index 73be3d00..bf6ba77b 100755 --- a/src/test/php/lang/ast/unittest/emit/Handle.class.php +++ b/src/test/php/lang/ast/unittest/emit/Handle.class.php @@ -4,9 +4,14 @@ /** Used by `UsingTest` */ class Handle implements \IDisposable { + public static $DEFAULT; public static $called= []; private $id; + static function __static() { + self::$DEFAULT= new Handle(0); + } + public function __construct($id) { $this->id= $id; } public function redirect($id) { diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php similarity index 76% rename from src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php rename to src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 0b52483d..f09bd8ec 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithNewTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -4,22 +4,34 @@ use unittest\{Assert, Expect, Test, Values}; /** - * New in initializers + * Initialize parameters and properties with arbitrary expressions * + * @see https://github.com/xp-framework/compiler/pull/104 * @see https://wiki.php.net/rfc/new_in_initializers */ -class InitializeWithNewTest extends EmittingTest { +class InitializeWithExpressionsTest extends EmittingTest { + + /** @return iterable */ + private function expressions() { + yield ['"test"', 'test']; + yield ['[1, 2, 3]', [1, 2, 3]]; + yield ['MODIFIER_PUBLIC', MODIFIER_PUBLIC]; + yield ['self::INITIAL', 'initial']; + yield ['Handle::$DEFAULT', Handle::$DEFAULT]; + yield ['new Handle(0)', new Handle(0)]; + yield ['[new Handle(0)]', [new Handle(0)]]; + } - #[Test] - public function property() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { - private $h= new Handle(0); + #[Test, Values('expressions')] + public function property($code, $expected) { + Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\Handle; class { + const INITIAL= "initial"; + private $h= %s; public function run() { return $this->h; } - }'); - Assert::equals(new Handle(0), $r); + }', $code))); } #[Test] From c5547f9cb781b0e63e5adc068d597f75d4cdbe78 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:12:17 +0100 Subject: [PATCH 380/926] Verify support for initializing properties to closures --- .../php/lang/ast/unittest/emit/Handle.class.php | 1 + .../InitializeWithExpressionsTest.class.php | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/Handle.class.php b/src/test/php/lang/ast/unittest/emit/Handle.class.php index bf6ba77b..6c394fba 100755 --- a/src/test/php/lang/ast/unittest/emit/Handle.class.php +++ b/src/test/php/lang/ast/unittest/emit/Handle.class.php @@ -16,6 +16,7 @@ public function __construct($id) { $this->id= $id; } public function redirect($id) { $this->id= $id; + return $this; } public function read($bytes= 8192) { diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index f09bd8ec..d061bf2f 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -1,13 +1,14 @@ run('class { + private $h= fn($arg) => $arg->redirect(1); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r(new Handle(0))); + } + #[Test] public function property_initialization_accessible_inside_constructor() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { From cfd747fc403731344ef7eb71e9e0c543615ea1b9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:16:17 +0100 Subject: [PATCH 381/926] Emit binary operations directly instead of deferring their initialization See https://wiki.php.net/rfc/const_scalar_exprs --- src/main/php/lang/ast/emit/PHP.class.php | 7 +++++-- .../unittest/emit/InitializeWithExpressionsTest.class.php | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0ef4aeef..f37a55f2 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ member instanceof Literal; + } else if ($node instanceof BinaryExpression) { + return $this->isConstant($node->left) && $this->isConstant($node->right); } return false; } diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index d061bf2f..368132b0 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -8,6 +8,7 @@ * * @see https://github.com/xp-framework/compiler/pull/104 * @see https://wiki.php.net/rfc/new_in_initializers + * @see https://wiki.php.net/rfc/const_scalar_exprs * @see https://wiki.php.net/rfc/calls_in_constant_expressions */ class InitializeWithExpressionsTest extends EmittingTest { From f37e1130f554a81ec70952287cee664ebd89d92b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:22:38 +0100 Subject: [PATCH 382/926] Verify using anonymous classes work See https://github.com/php/php-src/pull/6746#issuecomment-790473002 --- .../emit/InitializeWithExpressionsTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 368132b0..402e5c48 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -50,6 +50,19 @@ public function run() { Assert::equals(new Handle(1), $r(new Handle(0))); } + #[Test] + public function using_anonymous_classes() { + $r= $this->run('class { + private $h= new class() { public function pipe($h) { return $h->redirect(1); } }; + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r->pipe(new Handle(0))); + } + + #[Test] public function property_initialization_accessible_inside_constructor() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { From ed8144e05cbd2b4ea099e87e69e2247cd752c089 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:26:29 +0100 Subject: [PATCH 383/926] Verify both short (fn) and long (function) closures work --- .../emit/InitializeWithExpressionsTest.class.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 402e5c48..a1e60578 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -27,7 +27,7 @@ private function expressions() { } #[Test, Values('expressions')] - public function property($code, $expected) { + public function property($declaration, $expected) { Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\Handle; class { const INITIAL= "initial"; private $h= %s; @@ -35,18 +35,18 @@ public function property($code, $expected) { public function run() { return $this->h; } - }', $code))); + }', $declaration))); } - #[Test] - public function using_functions() { - $r= $this->run('class { - private $h= fn($arg) => $arg->redirect(1); + #[Test, Values(['fn($arg) => $arg->redirect(1)', 'function($arg) { return $arg->redirect(1); }'])] + public function using_closures($declaration) { + $r= $this->run(sprintf('class { + private $h= %s; public function run() { return $this->h; } - }'); + }', $declaration)); Assert::equals(new Handle(1), $r(new Handle(0))); } @@ -62,7 +62,6 @@ public function run() { Assert::equals(new Handle(1), $r->pipe(new Handle(0))); } - #[Test] public function property_initialization_accessible_inside_constructor() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { From 1235334c128132f919d1ea1dcc352d993c5307af Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:29:13 +0100 Subject: [PATCH 384/926] Verify closures can access $this --- .../emit/InitializeWithExpressionsTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index a1e60578..49d478e6 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -50,6 +50,19 @@ public function run() { Assert::equals(new Handle(1), $r(new Handle(0))); } + #[Test] + public function using_closures_referencing_this() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private $id= 1; + private $h= fn() => new Handle($this->id); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r()); + } + #[Test] public function using_anonymous_classes() { $r= $this->run('class { From 0019bb28b2a094b4dc88a06f3268ab3718235f0f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:32:21 +0100 Subject: [PATCH 385/926] Verify nested `new` statements work See https://externals.io/message/113347#113352 --- .../ast/unittest/emit/InitializeWithExpressionsTest.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 49d478e6..c7cf4c63 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -23,12 +23,13 @@ private function expressions() { yield ['self::INITIAL', 'initial']; yield ['Handle::$DEFAULT', Handle::$DEFAULT]; yield ['new Handle(0)', new Handle(0)]; + yield ['new FileInput(new Handle(0))', new FileInput(new Handle(0))]; yield ['[new Handle(0)]', [new Handle(0)]]; } #[Test, Values('expressions')] public function property($declaration, $expected) { - Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\Handle; class { + Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { const INITIAL= "initial"; private $h= %s; From fa3dd1a9122afcf49d4eb3da087120fb1dd14c74 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:39:15 +0100 Subject: [PATCH 386/926] Memory efficiency: Use integers instead of strings for deferred initializations --- src/main/php/lang/ast/emit/PHP.class.php | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f37a55f2..bc36d591 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -289,7 +289,7 @@ protected function emitParameter($result, $parameter) { $this->emitOne($result, $parameter->default); } else { $result->out->write('=null'); - $result->locals['$']['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; + $result->locals[1]['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; } } $result->locals[$parameter->name]= true; @@ -351,7 +351,7 @@ protected function emitLambda($result, $lambda) { protected function emitClass($result, $class) { array_unshift($result->meta, []); - $result->locals= ['$' => [], '@' => []]; + $result->locals= [[], []]; $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); $class->parent && $result->out->write(' extends '.$class->parent); @@ -363,14 +363,14 @@ protected function emitClass($result, $class) { // Create constructor for property initializations to non-static scalars // which have not already been emitted inside constructor - if ($result->locals['$']) { + if ($result->locals[1]) { $result->out->write('public function __construct() {'); - $this->emitInitializations($result, $result->locals['$']); + $this->emitInitializations($result, $result->locals[1]); $result->out->write('}'); } $result->out->write('static function __init() {'); - $this->emitInitializations($result, $result->locals['@']); + $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name.'::__init();'); } @@ -520,9 +520,9 @@ protected function emitProperty($result, $property) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { - $result->locals['@']['self::$'.$property->name]= $property->expression; + $result->locals[0]['self::$'.$property->name]= $property->expression; } else { - $result->locals['$']['$this->'.$property->name]= $property->expression; + $result->locals[1]['$this->'.$property->name]= $property->expression; } } $result->out->write(';'); @@ -532,11 +532,11 @@ protected function emitMethod($result, $method) { // Include non-static initializations for members in constructor, removing // them to signal emitClass() no constructor needs to be generated. - if ('__construct' === $method->name && isset($result->locals['$'])) { - $locals= ['this' => true, '$' => $result->locals['$']]; - $result->locals['$']= []; + if ('__construct' === $method->name && isset($result->locals[1])) { + $locals= ['this' => true, 1 => $result->locals[1]]; + $result->locals[1]= []; } else { - $locals= ['this' => true, '$' => []]; + $locals= ['this' => true, 1 => []]; } $result->stack[]= $result->locals; $result->locals= $locals; @@ -556,7 +556,7 @@ protected function emitMethod($result, $method) { foreach ($method->signature->parameters as $param) { if (isset($param->promote)) { $promoted.= $param->promote.' $'.$param->name.';'; - $result->locals['$']['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); + $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); $result->meta[0][self::PROPERTY][$param->name]= [ DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', DETAIL_ANNOTATIONS => [], @@ -573,7 +573,7 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'); - $this->emitInitializations($result, $result->locals['$']); + $this->emitInitializations($result, $result->locals[1]); $this->emitAll($result, $method->body); $result->out->write('}'); } From 9d00be12f8bc7883ed3ebb14bf63b27296734a4e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 12:41:07 +0100 Subject: [PATCH 387/926] Verify new statements can access $this --- .../emit/InitializeWithExpressionsTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index c7cf4c63..aa916b07 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -64,6 +64,19 @@ public function run() { Assert::equals(new Handle(1), $r()); } + #[Test] + public function using_new_referencing_this() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private $id= 1; + private $h= new Handle($this->id); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r); + } + #[Test] public function using_anonymous_classes() { $r= $this->run('class { From 96d45da05495891f25c654d879a4161e42510837 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 13:06:45 +0100 Subject: [PATCH 388/926] Verify reflective access to property works as expected --- .../emit/InitializeWithExpressionsTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index aa916b07..95660683 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -39,6 +39,18 @@ public function run() { }', $declaration))); } + #[Test, Values('expressions')] + public function reflective_access_to_property($declaration, $expected) { + Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { + const INITIAL= "initial"; + private $h= %s; + + public function run() { + return typeof($this)->getField("h")->get($this); + } + }', $declaration))); + } + #[Test, Values(['fn($arg) => $arg->redirect(1)', 'function($arg) { return $arg->redirect(1); }'])] public function using_closures($declaration) { $r= $this->run(sprintf('class { From 83707898d3e701e24ba36372738a56f56b5778ea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 17:59:52 +0100 Subject: [PATCH 389/926] Verify parameter default is accessible via reflection See xp-framework/core#259 --- src/main/php/lang/ast/emit/PHP.class.php | 9 +++++++-- .../emit/InitializeWithExpressionsTest.class.php | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index bc36d591..7afbf52b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -554,6 +554,9 @@ protected function emitMethod($result, $method) { $promoted= ''; foreach ($method->signature->parameters as $param) { + $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; + $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + if (isset($param->promote)) { $promoted.= $param->promote.' $'.$param->name.';'; $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); @@ -565,8 +568,10 @@ protected function emitMethod($result, $method) { DETAIL_ARGUMENTS => [] ]; } - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; - $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + + if (isset($param->default) && !$this->isConstant($param->default)) { + $meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default]; + } } if (null === $method->body) { diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 95660683..6aefa700 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -149,6 +149,16 @@ public function run($h= new Handle(0)) { Assert::equals(new Handle(1), $r); } + #[Test] + public function parameter_default_reflective_access() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run($h= new Handle(0)) { + return typeof($this)->getMethod("run")->getParameter(0)->getDefaultValue(); + } + }'); + Assert::equals(new Handle(0), $r); + } + #[Test] public function property_reference_as_parameter_default() { $r= $this->run('use lang\ast\unittest\emit\Handle; class { From 9fac33fd7a848ab03c7a46b9208d690fc255548c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Mar 2021 18:22:46 +0100 Subject: [PATCH 390/926] Prepare 6.2.0 release --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b199b15a..e835a4bf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.2.0 / 2021-03-06 + +* Merged PR #104: Support arbitrary expressions in property initializations + and parameter defaults + (@thekid) + ## 6.1.1 / 2021-01-04 * Fixed issue #102: Call to a member function children()... - @thekid From c0a0b71e24f8f2a1de2c1997b46ecfa0fd785518 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 09:36:42 +0100 Subject: [PATCH 391/926] Use named arguments for annotations --- .../lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- .../lang/ast/unittest/emit/ControlStructuresTest.class.php | 4 ++-- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 96c616da..7c115094 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -83,7 +83,7 @@ public function __construct(private int $id, private string $name) { } ); } - #[Test, Expect(['class' => Errors::class, 'withMessage' => 'Variadic parameters cannot be promoted'])] + #[Test, Expect(class: Errors::class, withMessage: 'Variadic parameters cannot be promoted')] public function variadic_parameters_cannot_be_promoted() { $this->type('class { public function __construct(private string... $in) { } diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index e803f0f2..fbd76d23 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -136,7 +136,7 @@ public function run($arg) { Assert::equals('10+ items', $r); } - #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] + #[Test, Expect(class: Throwable::class, withMessage: '/Unhandled match value of type .+/')] public function unhandled_match() { $this->run('class { public function run($arg) { @@ -149,7 +149,7 @@ public function run($arg) { }', SEEK_END); } - #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unknown seek mode .+/'])] + #[Test, Expect(class: Throwable::class, withMessage: '/Unknown seek mode .+/')] public function match_with_throw_expression() { $this->run('class { public function run($arg) { diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index af1613a8..0cde12f5 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -101,7 +101,7 @@ public function load_uri() { Assert::equals('Tests', $class->getSimpleName()); } - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => 'Compiler error: Expected "{", have "(end)"'])] + #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "{", have "(end)"')] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); }); } From 15f343daaa116d4a9d8eb85eb80bf4bc88f1679a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 09:38:57 +0100 Subject: [PATCH 392/926] Simplify no_longer_supports_hacklang_variant() test --- .../ast/unittest/emit/LambdasTest.class.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index f10fb253..4db484e8 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -178,17 +178,12 @@ public function run() { Assert::equals(2, $r()); } - #[Test] + #[Test, Expect(Errors::class)] public function no_longer_supports_hacklang_variant() { - try { - $this->run('class { - public function run() { - $func= ($arg) ==> { return 1; }; - } - }'); - $this->fail('No errors raised', null, Errors::class); - } catch (Errors $expected) { - \xp::gc(); - } + $this->run('class { + public function run() { + $func= ($arg) ==> { return 1; }; + } + }'); } } \ No newline at end of file From 6ed6c1c89b66dd1a784b802c4fcfd47e5bfd83a2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 14:25:48 +0100 Subject: [PATCH 393/926] Initial proof of concept --- src/main/php/lang/ast/Result.class.php | 19 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 4 ++++ .../ast/unittest/emit/MembersTest.class.php | 13 +++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 166d192c..6102986d 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,5 +1,7 @@ codegen->symbol(); } + + /** + * Looks up a given type + * + * @param string $type + * @return lang.ast.types.Type + */ + public function lookup($type) { + if ('self' === $type || 'static' === $type || $type === $this->type[0]->name) { + return new Declaration($this->type[0], $this); + } else if ('parent' === $type) { + return $this->lookup($this->type[0]->parent); + } else { + return new Reflection($type); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7afbf52b..1c89b771 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -350,6 +350,7 @@ protected function emitLambda($result, $lambda) { } protected function emitClass($result, $class) { + array_unshift($result->type, $class); array_unshift($result->meta, []); $result->locals= [[], []]; @@ -373,6 +374,7 @@ protected function emitClass($result, $class) { $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name.'::__init();'); + array_shift($result->type); } /** Stores lowercased, unnamespaced name in annotations for BC reasons! */ @@ -909,6 +911,8 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); + } else if ($scope->member instanceof Literal && '$enum' === $result->lookup($scope->type)->kind()) { + $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); $this->emitOne($result, $scope->member); diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 6807e5ef..beca43f1 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -171,6 +171,19 @@ public function run() { Assert::equals('MON', $r); } + #[Test] + public function allow_constant_syntax_for_members() { + $r= $this->run('use lang\{Enum, CommandLine}; class extends Enum { + public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; + + public function run() { + return [self::MON->name(), ::TUE->name(), CommandLine::WINDOWS->name()]; + } + }'); + + Assert::equals(['MON', 'TUE', 'WINDOWS'], $r); + } + #[Test] public function method_with_static() { $r= $this->run('class { From 42c806048876175c577fd5dd85fb370cf621ccf5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 8 Mar 2021 20:06:22 +0100 Subject: [PATCH 394/926] Add emitter for PHP enums See xp-framework/ast#23 --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 63 ++++++++++++++++--- .../lang/ast/unittest/emit/EnumTest.class.php | 53 ++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/EnumTest.class.php diff --git a/composer.json b/composer.json index 66cf5680..d4497400 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.0", + "xp-framework/ast": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1c89b771..5d0f4a85 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -40,21 +40,26 @@ protected function declaration($name) { * - Binary expression where left- and right hand side are literals * * @see https://wiki.php.net/rfc/const_scalar_exprs + * @param lang.ast.Result $result * @param lang.ast.Node $node * @return bool */ - protected function isConstant($node) { + protected function isConstant($result, $node) { if ($node instanceof Literal) { return true; } else if ($node instanceof ArrayLiteral) { foreach ($node->values as $node) { - if (!$this->isConstant($node)) return false; + if (!$this->isConstant($result, $node)) return false; } return true; } else if ($node instanceof ScopeExpression) { - return $node->member instanceof Literal; + return ( + $node->member instanceof Literal && + is_string($node->type) && + 'enum' !== $result->lookup($node->type)->kind() + ); } else if ($node instanceof BinaryExpression) { - return $this->isConstant($node->left) && $this->isConstant($node->right); + return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); } return false; } @@ -188,7 +193,7 @@ protected function emitStatic($result, $static) { foreach ($static->initializations as $variable => $initial) { $result->out->write('static $'.$variable); if ($initial) { - if ($this->isConstant($initial)) { + if ($this->isConstant($result, $initial)) { $result->out->write('='); $this->emitOne($result, $initial); } else { @@ -284,7 +289,7 @@ protected function emitParameter($result, $parameter) { $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); } if ($parameter->default) { - if ($this->isConstant($parameter->default)) { + if ($this->isConstant($result, $parameter->default)) { $result->out->write('='); $this->emitOne($result, $parameter->default); } else { @@ -349,6 +354,46 @@ protected function emitLambda($result, $lambda) { $this->emitOne($result, $lambda->body); } + protected function emitEnumCase($result, $case) { + $result->out->write('public static $'.$case->name.';'); + } + + protected function emitEnum($result, $enum) { + array_unshift($result->type, $enum); + array_unshift($result->meta, []); + $result->locals= [[], []]; + + $result->out->write('final class '.$this->declaration($enum->name).' implements \UnitEnum'); + $enum->implements && $result->out->write(', '.implode(', ', $enum->implements)); + $result->out->write('{'); + + $cases= []; + foreach ($enum->body as $member) { + if ($member->is('enumcase')) $cases[]= $member; + $this->emitOne($result, $member); + } + + // Name and constructor + $result->out->write('public $name; private function __construct($name) { $this->name= $name; }'); + + // Enum cases + $result->out->write('public static function cases() { return ['); + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.', '); + } + $result->out->write(']; }'); + + // Initializations + $result->out->write('static function __init() {'); + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + } + $this->emitInitializations($result, $result->locals[0]); + $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); + $result->out->write('}} '.$enum->name.'::__init();'); + array_shift($result->type); + } + protected function emitClass($result, $class) { array_unshift($result->type, $class); array_unshift($result->meta, []); @@ -518,7 +563,7 @@ protected function emitProperty($result, $property) { $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); if (isset($property->expression)) { - if ($this->isConstant($property->expression)) { + if ($this->isConstant($result, $property->expression)) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { @@ -571,7 +616,7 @@ protected function emitMethod($result, $method) { ]; } - if (isset($param->default) && !$this->isConstant($param->default)) { + if (isset($param->default) && !$this->isConstant($result, $param->default)) { $meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default]; } } @@ -911,7 +956,7 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && '$enum' === $result->lookup($scope->type)->kind()) { + } else if ($scope->member instanceof Literal && 'enum' === $result->lookup($scope->type)->kind()) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php new file mode 100755 index 00000000..893e8f62 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -0,0 +1,53 @@ + function_exists("enum_exists"))')] +class EnumTest extends EmittingTest { + + #[Test] + public function name_property() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return self::Hearts->name; + } + }'); + + Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + } + + #[Test] + public function cases_method() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + + #[Test] + public function used_as_parameter_default() { + $t= $this->type('enum { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('ASC', $t->getMethod('run')->invoke(null)); + } +} \ No newline at end of file From b1afdaec295768c46ead140b087d3e941a363725 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 10 Mar 2021 20:05:34 +0100 Subject: [PATCH 395/926] Test PHP 8.1 enum lookalikes in conjunction with Enum::valuesOf() / Enum::valueOf() --- src/main/php/lang/ast/emit/PHP.class.php | 4 +-- .../lang/ast/unittest/emit/EnumTest.class.php | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5d0f4a85..5dd4fd49 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -56,7 +56,7 @@ protected function isConstant($result, $node) { return ( $node->member instanceof Literal && is_string($node->type) && - 'enum' !== $result->lookup($node->type)->kind() + !$result->lookup($node->type)->isEnumCase($node->member->expression) ); } else if ($node instanceof BinaryExpression) { return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); @@ -956,7 +956,7 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && 'enum' === $result->lookup($scope->type)->kind()) { + } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->isEnumCase($scope->member->expression)) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 893e8f62..ad758521 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -50,4 +50,39 @@ public static function run($order= self::ASC) { Assert::equals('ASC', $t->getMethod('run')->invoke(null)); } + + #[Test] + public function enum_values() { + $t= $this->type('use lang\Enum; enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return Enum::valuesOf(self::class); + } + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('run')->invoke(null)) + ); + } + + #[Test] + public function enum_value() { + $t= $this->type('use lang\Enum; enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return Enum::valueOf(self::class, "Hearts")->name; + } + }'); + + Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + } } \ No newline at end of file From 327a40e9b3a13217195472a678a26f425c8affee Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 10 Mar 2021 20:23:43 +0100 Subject: [PATCH 396/926] Add missing lang.ast.types package --- .../php/lang/ast/types/Declaration.class.php | 34 ++++++++++++++++++ .../php/lang/ast/types/Reflection.class.php | 35 +++++++++++++++++++ src/main/php/lang/ast/types/Type.class.php | 15 ++++++++ 3 files changed, 84 insertions(+) create mode 100755 src/main/php/lang/ast/types/Declaration.class.php create mode 100755 src/main/php/lang/ast/types/Reflection.class.php create mode 100755 src/main/php/lang/ast/types/Type.class.php diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php new file mode 100755 index 00000000..09dfc9bc --- /dev/null +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -0,0 +1,34 @@ +type= $type; + $this->result= $result; + } + + /** @return string */ + public function name() { return ltrim($this->type->name, '\\'); } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function isEnumCase($member) { + if ('enum' === $this->type->kind) { + return ($this->type->body[$member] ?? null) instanceof EnumCase; + } else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) { + return ($this->type->body['$'.$member] ?? null) instanceof Property; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php new file mode 100755 index 00000000..ebc12b84 --- /dev/null +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -0,0 +1,35 @@ +reflect= new \ReflectionClass($type); + } catch (\ReflectionException $e) { + throw new ClassNotFoundException($type); + } + } + + /** @return string */ + public function name() { return $this->reflect->name; } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function isEnumCase($member) { + if ($this->reflect->isSubclassOf(Enum::class)) { + return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; + } else if ($this->reflect->isSubclassOf(\UnitEnum::class)) { + $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); + return $value instanceof \UnitEnum; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php new file mode 100755 index 00000000..d9e23b3e --- /dev/null +++ b/src/main/php/lang/ast/types/Type.class.php @@ -0,0 +1,15 @@ + Date: Wed, 10 Mar 2021 20:30:10 +0100 Subject: [PATCH 397/926] Require XP core 10.8.0+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4497400..7d86f528 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", + "xp-framework/core": "^10.8", "xp-framework/ast": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, From a528053b5505b7531a30b06609ad66bc5dd96baa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 10:11:55 +0100 Subject: [PATCH 398/926] Add support for backed enums (`class SortOrder: string { ... }`) --- src/main/php/lang/ast/emit/PHP.class.php | 38 ++++++++++-- .../lang/ast/unittest/emit/EnumTest.class.php | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5dd4fd49..f1af5edb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -363,7 +363,7 @@ protected function emitEnum($result, $enum) { array_unshift($result->meta, []); $result->locals= [[], []]; - $result->out->write('final class '.$this->declaration($enum->name).' implements \UnitEnum'); + $result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); $enum->implements && $result->out->write(', '.implode(', ', $enum->implements)); $result->out->write('{'); @@ -373,8 +373,28 @@ protected function emitEnum($result, $enum) { $this->emitOne($result, $member); } - // Name and constructor - $result->out->write('public $name; private function __construct($name) { $this->name= $name; }'); + // Constructors + if ($enum->base) { + $result->out->write('public $name, $value;'); + $result->out->write('private static $values= [];'); + $result->out->write('private function __construct($name, $value) { + $this->name= $name; + $this->value= $value; + self::$values[$value]= $this; + }'); + $result->out->write('public static function tryFrom($value) { + return self::$values[$value] ?? null; + }'); + $result->out->write('public static function from($value) { + if ($r= self::$values[$value] ?? null) return $r; + throw new \ValueError("Not an enum value: ".\util\Objects::stringOf($value)); + }'); + } else { + $result->out->write('public $name;'); + $result->out->write('private function __construct($name) { + $this->name= $name; + }'); + } // Enum cases $result->out->write('public static function cases() { return ['); @@ -385,8 +405,16 @@ protected function emitEnum($result, $enum) { // Initializations $result->out->write('static function __init() {'); - foreach ($cases as $case) { - $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + if ($enum->base) { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", '); + $this->emitOne($result, $case->expression); + $result->out->write(');'); + } + } else { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + } } $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ad758521..9855a32e 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -51,6 +51,67 @@ public static function run($order= self::ASC) { Assert::equals('ASC', $t->getMethod('run')->invoke(null)); } + #[Test] + public function value_property_of_backed_enum() { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run() { + return self::DESC->value; + } + }'); + + Assert::equals('desc', $t->getMethod('run')->invoke(null)); + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] + public function backed_enum_from($arg, $expected) { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run($arg) { + return self::from($arg)->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + + #[Test] + public function backed_enum_from_nonexistant() { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run() { + try { + self::from("illegal"); + throw new \lang\IllegalStateException("No exception raised"); + } catch (\ValueError $expected) { + return $expected->getMessage(); + } + } + }'); + + Assert::equals('Not an enum value: "illegal"', $t->getMethod('run')->invoke(null)); + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] + public function backed_enum_tryFrom($arg, $expected) { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run($arg) { + return self::tryFrom($arg)?->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + #[Test] public function enum_values() { $t= $this->type('use lang\Enum; enum { From 179b15b89af389a731d1fa328e839bf5d9c61818 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 10:43:24 +0100 Subject: [PATCH 399/926] Adjust error message to match PHP 8.1 implementation --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f1af5edb..2abad2bc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \ValueError("Not an enum value: ".\util\Objects::stringOf($value)); + throw new \ValueError(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 9855a32e..7c19eeaa 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -81,21 +81,24 @@ public static function run($arg) { #[Test] public function backed_enum_from_nonexistant() { - $t= $this->type('enum : string { + $t= $this->type('use lang\IllegalStateException; enum : string { case ASC = "asc"; case DESC = "desc"; public static function run() { try { self::from("illegal"); - throw new \lang\IllegalStateException("No exception raised"); + throw new IllegalStateException("No exception raised"); } catch (\ValueError $expected) { return $expected->getMessage(); } } }'); - Assert::equals('Not an enum value: "illegal"', $t->getMethod('run')->invoke(null)); + Assert::equals( + '"illegal" is not a valid backing value for enum "'.$t->literal().'"', + $t->getMethod('run')->invoke(null) + ); } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] From b12e5c15df2a6ca27552346da9547c2608143bcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:08:02 +0100 Subject: [PATCH 400/926] Emit PHP 8.1 native enums if support is available --- src/main/php/lang/ast/emit/PHP.class.php | 4 +- src/main/php/lang/ast/emit/PHP81.class.php | 212 ++++++++++++++++++ .../php/lang/ast/types/Declaration.class.php | 5 +- .../php/lang/ast/types/Reflection.class.php | 5 +- src/main/php/lang/ast/types/Type.class.php | 3 +- 5 files changed, 222 insertions(+), 7 deletions(-) create mode 100755 src/main/php/lang/ast/emit/PHP81.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 2abad2bc..9d786370 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -56,7 +56,7 @@ protected function isConstant($result, $node) { return ( $node->member instanceof Literal && is_string($node->type) && - !$result->lookup($node->type)->isEnumCase($node->member->expression) + !$result->lookup($node->type)->rewriteEnumCase($node->member->expression) ); } else if ($node instanceof BinaryExpression) { return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); @@ -984,7 +984,7 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->isEnumCase($scope->member->expression)) { + } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression)) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php new file mode 100755 index 00000000..9b9af638 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -0,0 +1,212 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); } + ]; + } + + /** + * Returns whether a given node is a constant expression: + * + * - Any literal + * - Arrays where all members are literals + * - Scope expressions with literal members (self::class, T::const) + * - Binary expression where left- and right hand side are literals + * + * @see https://wiki.php.net/rfc/const_scalar_exprs + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @return bool + */ + protected function isConstant($result, $node) { + if ($node instanceof Literal) { + return true; + } else if ($node instanceof ArrayLiteral) { + foreach ($node->values as $node) { + if (!$this->isConstant($result, $node)) return false; + } + return true; + } else if ($node instanceof ScopeExpression) { + return ( + $node->member instanceof Literal && + is_string($node->type) && + !$result->lookup($node->type)->rewriteEnumCase($node->member->expression, self::$ENUMS) + ); + } else if ($node instanceof BinaryExpression) { + return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); + } + return false; + } + + protected function emitArguments($result, $arguments) { + $i= 0; + foreach ($arguments as $name => $argument) { + if ($i++) $result->out->write(','); + if (is_string($name)) $result->out->write($name.':'); + $this->emitOne($result, $argument); + } + } + + protected function emitScope($result, $scope) { + if ($scope->type instanceof Variable) { + $this->emitOne($result, $scope->type); + $result->out->write('::'); + $this->emitOne($result, $scope->member); + } else if ($scope->type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'='); + $this->emitOne($result, $scope->type); + $result->out->write(')?'.$t.'::'); + $this->emitOne($result, $scope->member); + $result->out->write(':null'); + } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression, self::$ENUMS)) { + $result->out->write($scope->type.'::$'.$scope->member->expression); + } else { + $result->out->write($scope->type.'::'); + $this->emitOne($result, $scope->member); + } + } + + protected function emitEnumCase($result, $case) { + if (self::$ENUMS) { + $result->out->write('case '.$case->name); + if ($case->expression) { + $result->out->write('='); + $this->emitOne($result, $case->expression); + } + $result->out->write(';'); + } else { + parent::emitEnumCase($result, $case); + } + } + + protected function emitEnum($result, $enum) { + if (self::$ENUMS) { + array_unshift($result->type, $enum); + array_unshift($result->meta, []); + $result->locals= [[], []]; + + $result->out->write('enum '.$this->declaration($enum->name)); + $enum->base && $result->out->write(':'.$enum->base); + $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); + $result->out->write('{'); + + foreach ($enum->body as $member) { + $this->emitOne($result, $member); + } + + // Initializations + $result->out->write('static function __init() {'); + $this->emitInitializations($result, $result->locals[0]); + $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); + $result->out->write('}} '.$enum->name.'::__init();'); + array_shift($result->type); + } else { + parent::emitEnum($result, $enum); + } + } + + protected function emitNew($result, $new) { + if ($new->type instanceof Node) { + $result->out->write('new ('); + $this->emitOne($result, $new->type); + $result->out->write(')('); + } else { + $result->out->write('new '.$new->type.'('); + } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + + protected function emitThrowExpression($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + } + + protected function emitCatch($result, $catch) { + $capture= $catch->variable ? ' $'.$catch->variable : ''; + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable'.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + + protected function emitNullsafeInstance($result, $instance) { + $this->emitOne($result, $instance->expression); + $result->out->write('?->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + + protected function emitMatch($result, $match) { + if (null === $match->expression) { + $result->out->write('match (true) {'); + } else { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + } + + foreach ($match->cases as $case) { + $b= 0; + foreach ($case->expressions as $expression) { + $b && $result->out->write(','); + $this->emitOne($result, $expression); + $b++; + } + $result->out->write('=>'); + $this->emitAsExpression($result, $case->body); + $result->out->write(','); + } + + if ($match->default) { + $result->out->write('default=>'); + $this->emitAsExpression($result, $match->default); + } + + $result->out->write('}'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php index 09dfc9bc..85b2412e 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -21,10 +21,11 @@ public function name() { return ltrim($this->type->name, '\\'); } * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether native enum support exists * @return bool */ - public function isEnumCase($member) { - if ('enum' === $this->type->kind) { + public function rewriteEnumCase($member, $native= false) { + if (!$native && 'enum' === $this->type->kind) { return ($this->type->body[$member] ?? null) instanceof EnumCase; } else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) { return ($this->type->body['$'.$member] ?? null) instanceof Property; diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php index ebc12b84..1fb0e5c3 100755 --- a/src/main/php/lang/ast/types/Reflection.class.php +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -21,12 +21,13 @@ public function name() { return $this->reflect->name; } * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether enums are natively supported * @return bool */ - public function isEnumCase($member) { + public function rewriteEnumCase($member, $native= false) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if ($this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!$native && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php index d9e23b3e..4f092b09 100755 --- a/src/main/php/lang/ast/types/Type.class.php +++ b/src/main/php/lang/ast/types/Type.class.php @@ -9,7 +9,8 @@ public function name(); * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether native enum support exists * @return bool */ - public function isEnumCase($member); + public function rewriteEnumCase($member, $native= false); } \ No newline at end of file From 71379f9431263706d80d9edce85f0e0d4016c3b4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:10:03 +0100 Subject: [PATCH 401/926] Make compatible with PHP < 7.4 --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9d786370..0b9eb7d3 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \ValueError(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + throw new \Exception(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 7c19eeaa..6a80f077 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -89,7 +89,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\ValueError $expected) { + } catch (\Exception $expected) { return $expected->getMessage(); } } From ce24cebda2b40940434b3dac592350d28a928a77 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:14:13 +0100 Subject: [PATCH 402/926] Verify int-backed enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 6a80f077..6efdcaf7 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -65,8 +65,22 @@ public static function run() { Assert::equals('desc', $t->getMethod('run')->invoke(null)); } + #[Test, Values([[0, 'NO'], [1, 'YES']])] + public function backed_enum_from_int($arg, $expected) { + $t= $this->type('enum : int { + case NO = 0; + case YES = 1; + + public static function run($arg) { + return self::from($arg)->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] - public function backed_enum_from($arg, $expected) { + public function backed_enum_from_string($arg, $expected) { $t= $this->type('enum : string { case ASC = "asc"; case DESC = "desc"; @@ -89,7 +103,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\Exception $expected) { + } catch (\Throwable $expected) { return $expected->getMessage(); } } From 0503bde2a65999a8506469592c8232c9ec428704 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:18:35 +0100 Subject: [PATCH 403/926] Use \Error inside ::from() --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0b9eb7d3..1c4198e2 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \Exception(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 6efdcaf7..1e9b9123 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -103,7 +103,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\Throwable $expected) { + } catch (\Error $expected) { return $expected->getMessage(); } } From aca3aadbbf5478cdee9ae4832b81a833852a7f40 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:25:12 +0100 Subject: [PATCH 404/926] Move native enum support detection to types --- src/main/php/lang/ast/emit/PHP81.class.php | 65 +------------------ .../php/lang/ast/types/Declaration.class.php | 9 +-- .../php/lang/ast/types/Reflection.class.php | 9 +-- src/main/php/lang/ast/types/Type.class.php | 12 ++-- .../lang/ast/unittest/emit/EnumTest.class.php | 17 +++++ 5 files changed, 38 insertions(+), 74 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 9b9af638..b6586506 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -1,7 +1,7 @@ literal mappings */ public function __construct() { $this->literals= [ @@ -38,39 +32,6 @@ public function __construct() { ]; } - /** - * Returns whether a given node is a constant expression: - * - * - Any literal - * - Arrays where all members are literals - * - Scope expressions with literal members (self::class, T::const) - * - Binary expression where left- and right hand side are literals - * - * @see https://wiki.php.net/rfc/const_scalar_exprs - * @param lang.ast.Result $result - * @param lang.ast.Node $node - * @return bool - */ - protected function isConstant($result, $node) { - if ($node instanceof Literal) { - return true; - } else if ($node instanceof ArrayLiteral) { - foreach ($node->values as $node) { - if (!$this->isConstant($result, $node)) return false; - } - return true; - } else if ($node instanceof ScopeExpression) { - return ( - $node->member instanceof Literal && - is_string($node->type) && - !$result->lookup($node->type)->rewriteEnumCase($node->member->expression, self::$ENUMS) - ); - } else if ($node instanceof BinaryExpression) { - return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); - } - return false; - } - protected function emitArguments($result, $arguments) { $i= 0; foreach ($arguments as $name => $argument) { @@ -80,28 +41,8 @@ protected function emitArguments($result, $arguments) { } } - protected function emitScope($result, $scope) { - if ($scope->type instanceof Variable) { - $this->emitOne($result, $scope->type); - $result->out->write('::'); - $this->emitOne($result, $scope->member); - } else if ($scope->type instanceof Node) { - $t= $result->temp(); - $result->out->write('('.$t.'='); - $this->emitOne($result, $scope->type); - $result->out->write(')?'.$t.'::'); - $this->emitOne($result, $scope->member); - $result->out->write(':null'); - } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression, self::$ENUMS)) { - $result->out->write($scope->type.'::$'.$scope->member->expression); - } else { - $result->out->write($scope->type.'::'); - $this->emitOne($result, $scope->member); - } - } - protected function emitEnumCase($result, $case) { - if (self::$ENUMS) { + if (Type::$ENUMS) { $result->out->write('case '.$case->name); if ($case->expression) { $result->out->write('='); @@ -114,7 +55,7 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { - if (self::$ENUMS) { + if (Type::$ENUMS) { array_unshift($result->type, $enum); array_unshift($result->meta, []); $result->locals= [[], []]; diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php index 85b2412e..954ae39c 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -2,9 +2,11 @@ use lang\ast\nodes\{EnumCase, Property}; -class Declaration implements Type { +class Declaration extends Type { private $type, $result; + static function __static() { } + /** * @param lang.ast.nodes.TypeDeclaration $type * @param lang.ast.Result $result @@ -21,11 +23,10 @@ public function name() { return ltrim($this->type->name, '\\'); } * Returns whether a given member is an enum case * * @param string $member - * @param bool $native Whether native enum support exists * @return bool */ - public function rewriteEnumCase($member, $native= false) { - if (!$native && 'enum' === $this->type->kind) { + public function rewriteEnumCase($member) { + if (!self::$ENUMS && 'enum' === $this->type->kind) { return ($this->type->body[$member] ?? null) instanceof EnumCase; } else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) { return ($this->type->body['$'.$member] ?? null) instanceof Property; diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php index 1fb0e5c3..fecc2b1d 100755 --- a/src/main/php/lang/ast/types/Reflection.class.php +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -2,9 +2,11 @@ use lang\{Enum, ClassNotFoundException}; -class Reflection implements Type { +class Reflection extends Type { private $reflect; + static function __static() { } + /** @param string $type */ public function __construct($type) { try { @@ -21,13 +23,12 @@ public function name() { return $this->reflect->name; } * Returns whether a given member is an enum case * * @param string $member - * @param bool $native Whether enums are natively supported * @return bool */ - public function rewriteEnumCase($member, $native= false) { + public function rewriteEnumCase($member) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if (!$native && $this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!self::$ENUMS && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php index 4f092b09..f2f5b2ba 100755 --- a/src/main/php/lang/ast/types/Type.class.php +++ b/src/main/php/lang/ast/types/Type.class.php @@ -1,16 +1,20 @@ type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + const COLORS = ["red", "black"]; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + #[Test] public function used_as_parameter_default() { $t= $this->type('enum { From 24f95be10e810ac005c67b9f6b4c5484d5adbc20 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:27:02 +0100 Subject: [PATCH 405/926] Move types to lang.ast.emit package --- src/main/php/lang/ast/Result.class.php | 4 ++-- src/main/php/lang/ast/{types => emit}/Declaration.class.php | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 1 - src/main/php/lang/ast/{types => emit}/Reflection.class.php | 2 +- src/main/php/lang/ast/{types => emit}/Type.class.php | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) rename src/main/php/lang/ast/{types => emit}/Declaration.class.php (96%) rename src/main/php/lang/ast/{types => emit}/Reflection.class.php (96%) rename src/main/php/lang/ast/{types => emit}/Type.class.php (92%) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 6102986d..310d7b74 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,6 +1,6 @@ type[0]->name) { diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php similarity index 96% rename from src/main/php/lang/ast/types/Declaration.class.php rename to src/main/php/lang/ast/emit/Declaration.class.php index 954ae39c..b8384f93 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,4 +1,4 @@ - Date: Sat, 13 Mar 2021 11:30:46 +0100 Subject: [PATCH 406/926] Use lang.Enum directly --- .../lang/ast/unittest/emit/EnumTest.class.php | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index c945c110..13ddb13b 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -1,5 +1,6 @@ getMethod('run')->invoke(null)); } + #[Test] + public function overwritten_parameter_default_value() { + $t= $this->type('enum { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('DESC', $t->getMethod('run')->invoke(null, [Enum::valueOf($t, 'DESC')])); + } + #[Test] public function value_property_of_backed_enum() { $t= $this->type('enum : string { @@ -148,36 +163,28 @@ public static function run($arg) { #[Test] public function enum_values() { - $t= $this->type('use lang\Enum; enum { + $t= $this->type('enum { case Hearts; case Diamonds; case Clubs; case Spades; - - public static function run() { - return Enum::valuesOf(self::class); - } }'); Assert::equals( ['Hearts', 'Diamonds', 'Clubs', 'Spades'], - array_map(function($suit) { return $suit->name; }, $t->getMethod('run')->invoke(null)) + array_map(function($suit) { return $suit->name; }, Enum::valuesOf($t)) ); } #[Test] public function enum_value() { - $t= $this->type('use lang\Enum; enum { + $t= $this->type('enum { case Hearts; case Diamonds; case Clubs; case Spades; - - public static function run() { - return Enum::valueOf(self::class, "Hearts")->name; - } }'); - Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + Assert::equals('Hearts', Enum::valueOf($t, 'Hearts')->name); } } \ No newline at end of file From 1678c2079cb586842e12f36d4da72662d99dc157 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:34:31 +0100 Subject: [PATCH 407/926] Rephrase TODO comment --- src/main/php/lang/ast/emit/Type.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 3b383410..211763d7 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -4,7 +4,9 @@ abstract class Type { public static $ENUMS; static function __static() { - self::$ENUMS= class_exists(\ReflectionEnum::class, false); // TODO remove once enum PR is merged + + // TODO: Check PHP version ID once enum PR is merged + self::$ENUMS= class_exists(\ReflectionEnum::class, false); } /** @return string */ From 54e0997718d1971fc1704f7c6bea188ecbdd8634 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:36:30 +0100 Subject: [PATCH 408/926] Invoke from() directly --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 13ddb13b..ef3c78f1 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -102,13 +102,9 @@ public function backed_enum_from_int($arg, $expected) { $t= $this->type('enum : int { case NO = 0; case YES = 1; - - public static function run($arg) { - return self::from($arg)->name; - } }'); - Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] @@ -116,13 +112,9 @@ public function backed_enum_from_string($arg, $expected) { $t= $this->type('enum : string { case ASC = "asc"; case DESC = "desc"; - - public static function run($arg) { - return self::from($arg)->name; - } }'); - Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); } #[Test] From 80a531e8cdb30eb4dfe20fadf17163a2a8ad9bbb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:41:58 +0100 Subject: [PATCH 409/926] Verify XPClass::isEnum() --- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ef3c78f1..ab46ecd5 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -7,6 +7,11 @@ #[Action(eval: 'new VerifyThat(fn() => function_exists("enum_exists"))')] class EnumTest extends EmittingTest { + #[Test] + public function enum_type() { + Assert::true($this->type('enum { }')->isEnum()); + } + #[Test] public function name_property() { $t= $this->type('enum { From c929bf95ba40c6dcbc5cdcf4aeddb124b6642a51 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:48:21 +0100 Subject: [PATCH 410/926] Verify instance methods work on enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ab46ecd5..461b2981 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -158,6 +158,29 @@ public static function run($arg) { Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); } + #[Test] + public function declare_method_on_enum() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public function color() { + return match ($this) { + self::Hearts, self::Diamonds => "red", + self::Clubs, self::Spaces => "black", + }; + } + + public static function run() { + return self::Hearts->color(); + } + }'); + + Assert::equals('red', $t->getMethod('run')->invoke(null)); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 68b783d83e2dfbf981c9e86faff3bbe1e7c4b47a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:48:31 +0100 Subject: [PATCH 411/926] Verify enums can implement interfaces --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 461b2981..795df0f5 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -181,6 +181,18 @@ public static function run() { Assert::equals('red', $t->getMethod('run')->invoke(null)); } + #[Test] + public function enum_implementing_interface() { + $t= $this->type('use lang\Closeable; enum implements Closeable { + case File; + case Stream; + + public function close() { } + }'); + + Assert::true($t->isSubclassOf('lang.Closeable')); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 80958e64d74b30cb67da7f879d83220eb40a156a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:07:45 +0100 Subject: [PATCH 412/926] Add TODO comments --- src/main/php/lang/ast/emit/PHP81.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index a8cb57b3..6d8d5693 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -41,6 +41,9 @@ protected function emitArguments($result, $arguments) { } protected function emitEnumCase($result, $case) { + + // TODO: Once enum PR is merged, remove this conditional and refactor the + // code into a `RewriteEnums` trait to be included for all other versions if (Type::$ENUMS) { $result->out->write('case '.$case->name); if ($case->expression) { @@ -54,6 +57,9 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { + + // TODO: Once enum PR is merged, remove this conditional and refactor the + // code into a `RewriteEnums` trait to be included for all other versions if (Type::$ENUMS) { array_unshift($result->type, $enum); array_unshift($result->meta, []); From e83972ac53e7e9142dba9bd1807a6a66dafc91aa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:14:57 +0100 Subject: [PATCH 413/926] Verify enums and enum cases can be annotated However, since XP core reflection does not support constant annotations, we cannot access case annotations. This will be covered by the reflection library! --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 795df0f5..307f1777 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -193,6 +193,18 @@ public function close() { } Assert::true($t->isSubclassOf('lang.Closeable')); } + #[Test] + public function enum_annotations() { + $t= $this->type('#[Test] enum { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[Test, Ignore('XP core reflection does not support constant annotations')] + public function enum_member_annotations() { + $t= $this->type('enum { #[Test] case ONE; }'); + Assert::equals(['test' => null], $t->getConstant('ONE')->getAnnotations()); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 2fc6a5b4168e50ae79cf0a57251d2b4787362a60 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:18:19 +0100 Subject: [PATCH 414/926] Verify cases() for backed enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 307f1777..99630822 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -29,7 +29,7 @@ public static function run() { } #[Test] - public function cases_method() { + public function cases_method_for_unit_enums() { $t= $this->type('enum { case Hearts; case Diamonds; @@ -43,6 +43,21 @@ public function cases_method() { ); } + #[Test] + public function cases_method_for_backed_enums() { + $t= $this->type('enum : string { + case Hearts = "♥"; + case Diamonds = "♦"; + case Clubs = "♣"; + case Spades = "♠"; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + #[Test] public function cases_method_does_not_yield_constants() { $t= $this->type('enum { From 786299f8ccdf6ea88f90fce130e6b8ae1051d9c2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:36:21 +0100 Subject: [PATCH 415/926] Restore compatibility with XP < 10.8.0 --- composer.json | 2 +- src/main/php/lang/ast/emit/Reflection.class.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 7d86f528..d4497400 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.8", + "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/ast": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 35cbedcb..f28568d9 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -4,8 +4,11 @@ class Reflection extends Type { private $reflect; + private static $UNITENUM; - static function __static() { } + static function __static() { + self::$UNITENUM= interface_exists(\UnitEnum::class, false); // Compatibility with XP < 10.8.0 + } /** @param string $type */ public function __construct($type) { @@ -28,7 +31,7 @@ public function name() { return $this->reflect->name; } public function rewriteEnumCase($member) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if (!self::$ENUMS && $this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } From d4476daf6b786492d196787972a6fec22c4e098c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 16:45:54 +0100 Subject: [PATCH 416/926] Use release version for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4497400..1ca12cae 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/php-enums as 7.1.0", + "xp-framework/ast": "^7.1", "php" : ">=7.0.0" }, "require-dev" : { From 4d705e6c994f198ba66b591fc13de8c785b7b36d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 16:50:06 +0100 Subject: [PATCH 417/926] Release 6.3.0 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index e835a4bf..2b938349 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.3.0 / 2021-03-13 + +* Merged PR #106: Compile PHP enums to PHP 7/8 lookalikes, PHP 8.1 native. + (@thekid) + ## 6.2.0 / 2021-03-06 * Merged PR #104: Support arbitrary expressions in property initializations From af213e322b8f467eb8feb7f41c39b627bc22cf57 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 16:58:31 +0100 Subject: [PATCH 418/926] Add PHP 8.1 emitter --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 120274a7..bfc628f7 100755 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The -q option suppresses all diagnostic output except for errors. Features supported ------------------ -XP Compiler supports features such as annotations, arrow functions, property type-hints, the null-safe instance operator as well as all PHP 7 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). +XP Compiler supports features such as annotations, arrow functions, enums property type-hints, the null-safe instance operator as well as all PHP 7 and PHP 8 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). Additional syntax can be added by installing compiler plugins from [here](https://github.com/xp-lang): @@ -86,6 +86,7 @@ lang.ast.emit.PHP71 lang.ast.emit.PHP72 lang.ast.emit.PHP74 lang.ast.emit.PHP80 [*] +lang.ast.emit.PHP81 lang.ast.syntax.php.Using [*] @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> From 7c89541cc81b9f45e8d7ef4c649b0a5c3366c991 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 Mar 2021 10:01:45 +0100 Subject: [PATCH 419/926] Fix cloning --- ChangeLog.md | 5 +++++ src/main/php/lang/ast/emit/PHP.class.php | 7 ++++++- .../lang/ast/unittest/emit/EnumTest.class.php | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 2b938349..f6d93ec2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.3.1 / 2021-03-14 + +* Fix being able to clone enum lookalikes - @thekid +* Fix `clone` operator - @thekid + ## 6.3.0 / 2021-03-13 * Merged PR #106: Compile PHP enums to PHP 7/8 lookalikes, PHP 8.1 native. diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1c4198e2..73c6be40 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -396,6 +396,11 @@ protected function emitEnum($result, $enum) { }'); } + // Prevent cloning enums + $result->out->write('public function __clone() { + throw new \Error("Trying to clone an uncloneable object of class ".self::class); + }'); + // Enum cases $result->out->write('public static function cases() { return ['); foreach ($cases as $case) { @@ -676,7 +681,7 @@ protected function emitBinary($result, $binary) { } protected function emitPrefix($result, $unary) { - $result->out->write($unary->operator); + $result->out->write($unary->operator.' '); $this->emitOne($result, $unary->expression); } diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 99630822..8af47e5e 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -220,6 +220,27 @@ public function enum_member_annotations() { Assert::equals(['test' => null], $t->getConstant('ONE')->getAnnotations()); } + #[Test] + public function cannot_be_cloned() { + $t= $this->type('use lang\IllegalStateException; enum { + case ONE; + + public static function run() { + try { + return clone self::ONE; + throw new IllegalStateException("No exception raised"); + } catch (\Error $expected) { + return $expected->getMessage(); + } + } + }'); + + Assert::equals( + 'Trying to clone an uncloneable object of class '.$t->literal(), + $t->getMethod('run')->invoke(null) + ); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 6542551bb441812258f1e587da2e4bd7334f289a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 Mar 2021 10:55:30 +0100 Subject: [PATCH 420/926] Allow `new class() extends self` inside class declarations --- ChangeLog.md | 4 ++++ src/main/php/lang/ast/emit/PHP.class.php | 13 +++++++++++-- .../ast/unittest/emit/AnonymousClassTest.class.php | 10 ++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index f6d93ec2..078df32f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.3.2 / 2021-03-14 + +* Allowed `new class() extends self` inside class declarations - @thekid + ## 6.3.1 / 2021-03-14 * Fix being able to clone enum lookalikes - @thekid diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 73c6be40..687246a8 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -957,10 +957,17 @@ protected function emitNewClass($result, $new) { $this->emitArguments($result, $new->arguments); $result->out->write(')'); - $new->definition->parent && $result->out->write(' extends '.$new->definition->parent); + // Allow "extends self" to reference enclosing class (except if this + // class is an anonymous class!) + if ('self' === $new->definition->parent && $result->type[0]->name) { + $result->out->write(' extends '.$result->type[0]->name); + } else if ($new->definition->parent) { + $result->out->write(' extends '.$new->definition->parent); + } $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); - $result->out->write('{'); + array_unshift($result->type, $new->definition); + $result->out->write('{'); foreach ($new->definition->body as $member) { $this->emitOne($result, $member); $result->out->write("\n"); @@ -968,6 +975,8 @@ protected function emitNewClass($result, $new) { $result->out->write('function __new() {'); $this->emitMeta($result, null, [], null); $result->out->write('return $this; }})->__new()'); + + array_shift($result->type); } protected function emitInvoke($result, $invoke) { diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index dd6dbd60..fdedbe2e 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -66,4 +66,14 @@ public function fixture() { } Assert::equals(['inside' => null], typeof($r)->getMethod('fixture')->getAnnotations()); } + + #[Test] + public function extending_enclosing_class() { + $t= $this->type('class { + public static function run() { + return new class() extends self { }; + } + }'); + Assert::instance($t, $t->getMethod('run')->invoke(null)); + } } \ No newline at end of file From e84cd4757ae7805ee95f9dca5685bde557adb38b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 Mar 2021 11:01:21 +0100 Subject: [PATCH 421/926] Fix "lang.IndexOutOfBoundsException (Undefined array key 0)" --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 687246a8..1619a91c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -959,7 +959,7 @@ protected function emitNewClass($result, $new) { // Allow "extends self" to reference enclosing class (except if this // class is an anonymous class!) - if ('self' === $new->definition->parent && $result->type[0]->name) { + if ('self' === $new->definition->parent && $result->type && $result->type[0]->name) { $result->out->write(' extends '.$result->type[0]->name); } else if ($new->definition->parent) { $result->out->write(' extends '.$new->definition->parent); From 1f9e415236ad75cd119decbac965a4c133e543a2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 Mar 2021 11:17:16 +0100 Subject: [PATCH 422/926] Search all enclosing scopes inside lookup() --- src/main/php/lang/ast/Result.class.php | 10 +++++++--- .../ast/unittest/emit/AnonymousClassTest.class.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 310d7b74..3db2ede2 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -39,12 +39,16 @@ public function temp() { * @return lang.ast.emit.Type */ public function lookup($type) { - if ('self' === $type || 'static' === $type || $type === $this->type[0]->name) { + if ('self' === $type || 'static' === $type) { return new Declaration($this->type[0], $this); } else if ('parent' === $type) { return $this->lookup($this->type[0]->parent); - } else { - return new Reflection($type); } + + foreach ($this->type as $enclosing) { + if ($type === $enclosing->name) return new Declaration($enclosing, $this); + } + + return new Reflection($type); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index fdedbe2e..805c6bdf 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -76,4 +76,18 @@ public static function run() { }'); Assert::instance($t, $t->getMethod('run')->invoke(null)); } + + #[Test] + public function referencing_enclosing_class() { + $t= $this->type('class { + const ID = 6100; + + public static function run() { + return new class() extends self { + public static $id = ::ID; + }; + } + }'); + Assert::instance($t, $t->getMethod('run')->invoke(null)); + } } \ No newline at end of file From 31fdbce1d4b01904bf8fdeeb05d908afa8c3b974 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 16 Mar 2021 19:03:15 +0100 Subject: [PATCH 423/926] QA: More concise wording [skip ci] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bfc628f7..09a9b0b7 100755 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Compiles future PHP to today's PHP. Usage ----- -After adding the compiler to your project via `composer require xp-framework/compiler` classes will be passed through the compiler during autoloading. Code inside files with a *.class.php* ending is considered already compiled; files need to renamed `T.class.php` => `T.php` in order to be picked up. +After adding the compiler to your project via `composer require xp-framework/compiler`, it will hook into the class loading chain and compile `.php`-files on-demand. This keeps the efficient code-save-reload/rerun development process typical for PHP. Example ------- @@ -41,7 +41,7 @@ class HelloWorld { Compilation ----------- -By default, XP Compiler will hook into the class loading chain and compile files on-demand. This keeps the code-save-reload/rerun development process typical for PHP. However, compilation can also be performed manually by invoking the compiler: +Compilation can also be performed explicitely by invoking the compiler: ```bash # Compile code and write result to a class file From 3ddfdbbd64171f0097e7866b52d1ac697e820a31 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Apr 2021 20:45:11 +0200 Subject: [PATCH 424/926] Link to PHP RFC allowing `fn() => { ... }` See https://wiki.php.net/rfc/auto-capture-closure --- src/test/php/lang/ast/unittest/emit/LambdasTest.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 4db484e8..aee46bc9 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -7,6 +7,7 @@ * Lambdas (a.k.a. arrow functions) support * * @see https://wiki.php.net/rfc/arrow_functions_v2 + * @see https://wiki.php.net/rfc/auto-capture-closure */ class LambdasTest extends EmittingTest { From 98dfd38a12801e379539cfe1f90b955b785a95b6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Apr 2021 20:56:38 +0200 Subject: [PATCH 425/926] Add test verifying capturing with block fn-lambdas work --- .../lang/ast/unittest/emit/LambdasTest.class.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index aee46bc9..3cdaa742 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -179,6 +179,20 @@ public function run() { Assert::equals(2, $r()); } + #[Test] + public function capturing_with_block() { + $r= $this->run('class { + public function run() { + $a= 1; + return fn() => { + return $a + 1; + }; + } + }'); + + Assert::equals(2, $r()); + } + #[Test, Expect(Errors::class)] public function no_longer_supports_hacklang_variant() { $this->run('class { From 9474f3c3bdd352d4c2eafebdb7be6cc16e180e09 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 7 Apr 2021 20:57:04 +0200 Subject: [PATCH 426/926] Rewrite `never` return type to `void` in PHP < 8.1 --- 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 | 5 +++- .../ast/unittest/TypeLiteralsTest.class.php | 28 ++++++++++++++++++- 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 06de95a1..4032fa26 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -22,7 +22,7 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); - return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l) ? null : $l; + return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l || 'never' === $l) ? null : $l; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 4577e5b1..2c9f4640 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -22,7 +22,7 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); - return ('object' === $l || 'mixed' === $l) ? null : $l; + return ('object' === $l || 'mixed' === $l) ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 44275343..d717e5f7 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -22,7 +22,7 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); - return 'mixed' === $l ? null : $l; + return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 95fdb843..1e0918fa 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -21,7 +21,7 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsLiteral::class => function($t) { $l= $t->literal(); - return 'mixed' === $l ? null : $l; + return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index bb1bfacd..d8205039 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -27,7 +27,10 @@ public function __construct() { } return substr($u, 1); }, - IsLiteral::class => function($t) { return $t->literal(); } + IsLiteral::class => function($t) { + $l= $t->literal(); + return 'never' === $l ? 'void' : $l; + } ]; } diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index 2d2141d6..e76ac535 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -1,6 +1,6 @@ base(); yield [new IsLiteral('object'), null]; yield [new IsLiteral('void'), null]; + yield [new IsLiteral('never'), null]; yield [new IsLiteral('iterable'), null]; yield [new IsLiteral('mixed'), null]; yield [new IsNullable(new IsLiteral('string')), null]; @@ -39,6 +40,7 @@ private function php71() { yield from $this->base(); yield [new IsLiteral('object'), null]; yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'void']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; @@ -55,6 +57,7 @@ private function php72() { yield from $this->base(); yield [new IsLiteral('object'), 'object']; yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'void']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; @@ -80,6 +83,24 @@ private function php80() { yield from $this->base(); yield [new IsLiteral('object'), 'object']; yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + } + + /** + * PHP 8.1 added `never` + * + * @return iterable + */ + private function php81() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'never']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), 'mixed']; yield [new IsNullable(new IsLiteral('string')), '?string']; @@ -111,4 +132,9 @@ public function php74_literals($type, $literal) { public function php80_literals($type, $literal) { Assert::equals($literal, (new PHP80())->literal($type)); } + + #[Test, Values('php81')] + public function php81_literals($type, $literal) { + Assert::equals($literal, (new PHP81())->literal($type)); + } } \ No newline at end of file From 835ad635fe10faa99d20c72005aedc6fab2feafd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 Apr 2021 17:59:41 +0200 Subject: [PATCH 427/926] Verify sending into a generator works Fix released in https://github.com/xp-framework/ast/releases/tag/v7.1.1 --- .../ast/unittest/emit/YieldTest.class.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index dc3c7506..1ba186aa 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -76,4 +76,23 @@ public function run() { }'); Assert::equals([1, 2, 3, 4], iterator_to_array($r, false)); } + + #[Test] + public function yield_send() { + $r= $this->run('class { + public function run() { + while ($line= yield) { + echo $line, "\n"; + } + } + }'); + + ob_start(); + + $r->send('Hello'); + $r->send('World'); + $r->send(null); + + Assert::equals("Hello\nWorld\n", ob_get_clean()); + } } \ No newline at end of file From ca388a426990aef202473d87c999b92ca2bcf0c8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Apr 2021 10:52:27 +0200 Subject: [PATCH 428/926] Bump dependency version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1ca12cae..9738b7ff 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.1", + "xp-framework/ast": "^7.2", "php" : ">=7.0.0" }, "require-dev" : { From bcf4cebf01686faca7ac0fb85b2dd9eaf9950889 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Apr 2021 10:57:50 +0200 Subject: [PATCH 429/926] Release 6.4.0 --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 078df32f..d084000c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.4.0 / 2021-04-25 + +* Merged PR #110: Rewrite `never` return type to void in PHP < 8.1, adding + support for this PHP 8.1 feature. XP Framework reflection supports this + as of its 10.10.0 release. + (@thekid) + ## 6.3.2 / 2021-03-14 * Allowed `new class() extends self` inside class declarations - @thekid From 0d7326d7c480fc7e59cfe9994dadb009d5ea889b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Apr 2021 11:05:59 +0200 Subject: [PATCH 430/926] Download runners from Balto repo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e68daadb..2dddd0f1 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.2.0.sh > xp-run && + curl -sSL =https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From 3de2dc1659e9bfe93285e180ebde3ead80de0097 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Apr 2021 11:06:49 +0200 Subject: [PATCH 431/926] Fix typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dddd0f1..ef245d7f 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: > - curl -sSL =https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run && + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From e309eccbe542888aa828956f98f07c4872a5e05c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 May 2021 15:08:38 +0200 Subject: [PATCH 432/926] Add support for directives using `declare` See https://www.php.net/manual/en/control-structures.declare.php and https://www.php.net/manual/en/language.types.declarations.php --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 9 ++++++ .../ast/unittest/emit/DeclareTest.class.php | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/DeclareTest.class.php diff --git a/composer.json b/composer.json index 9738b7ff..e05d7dd4 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.2", + "xp-framework/ast": "dev-feature/declare as 7.3.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1619a91c..02207b71 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -157,6 +157,15 @@ protected function emitAsExpression($result, $expression) { } } + protected function emitDirectives($result, $directives) { + $result->out->write('declare('); + foreach ($directives->declare as $directive => $value) { + $result->out->write($directive.'='); + $this->emitOne($result, $value); + } + $result->out->write(')'); + } + protected function emitNamespace($result, $declaration) { $result->out->write('namespace '.$declaration->name); } diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php new file mode 100755 index 00000000..a5e2a6f2 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -0,0 +1,31 @@ +run('class { + public static function number(int $n) { return $n; } + public function run() { return self::number(1.5); } + }')); + } + + #[Test] + public function strict_types_off() { + Assert::equals(1, $this->run('declare(strict_types = 0); class { + public static function number(int $n) { return $n; } + public function run() { return self::number(1.5); } + }')); + } + + #[Test, Expect(class: Error::class, withMessage: '/must be of type int, float given/')] + public function strict_types_on() { + $this->run('declare(strict_types = 1); class { + public static function number(int $n) { return $n; } + public function run() { return self::number(1.5); } + }'); + } +} From 47048d19ec5d85d194786076ea0285e30987ee41 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 May 2021 15:12:38 +0200 Subject: [PATCH 433/926] Adjust error message expectancy for PHP 7 --- src/test/php/lang/ast/unittest/emit/DeclareTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php index a5e2a6f2..8cef3e6c 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -21,7 +21,7 @@ public function run() { return self::number(1.5); } }')); } - #[Test, Expect(class: Error::class, withMessage: '/must be of type int, float given/')] + #[Test, Expect(class: Error::class, withMessage: '/must be of (the )?type int(eger)?, float given/')] public function strict_types_on() { $this->run('declare(strict_types = 1); class { public static function number(int $n) { return $n; } From afdfecdbeadac60a5a230f4119ddd6152274bbf8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 May 2021 15:19:15 +0200 Subject: [PATCH 434/926] Use XP Runner from Balto repo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index adc7e6f3..a19f0401 100755 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - php: master before_script: - - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.1.7.sh > xp-run + - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run - composer install --prefer-dist - echo "vendor/autoload.php" > composer.pth From 858cfac1d5d1f8c61606793fd55fca1d529fdde1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 May 2021 15:28:24 +0200 Subject: [PATCH 435/926] Bump AST library dependency version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e05d7dd4..eada459d 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/declare as 7.3.0", + "xp-framework/ast": "^7.3", "php" : ">=7.0.0" }, "require-dev" : { From 19e21242400f2fad051f2deb3f75e132c49e8fb1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 22 May 2021 15:29:02 +0200 Subject: [PATCH 436/926] Release 6.5.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d084000c..0620677c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.5.0 / 2021-05-22 + +* Merged PR #111: Add support for directives using declare - @thekid + ## 6.4.0 / 2021-04-25 * Merged PR #110: Rewrite `never` return type to void in PHP < 8.1, adding From 3f592d81be534f2125798eccf4142783eb2de497 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 3 Jun 2021 21:38:41 +0200 Subject: [PATCH 437/926] QA: Implement lang.Value --- src/test/php/lang/ast/unittest/emit/Handle.class.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/Handle.class.php b/src/test/php/lang/ast/unittest/emit/Handle.class.php index 6c394fba..d4c066bf 100755 --- a/src/test/php/lang/ast/unittest/emit/Handle.class.php +++ b/src/test/php/lang/ast/unittest/emit/Handle.class.php @@ -1,9 +1,9 @@ id.'>'; } + + public function hashCode() { return '#'.$this->id; } + + public function compareTo($value) { return $value instanceof self ? $value->id <=> $this->id : 1; } + public function __dispose() { self::$called[]= '__dispose@'.$this->id; } From ee056bed48d1ce36fa3469cbac344008aa619e8f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 7 Jun 2021 18:27:11 +0200 Subject: [PATCH 438/926] Fix "Implicit conversion from non-compatible float" warnings See #112, occurs in PHP 8.1 only --- src/test/php/lang/ast/unittest/emit/DeclareTest.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php index 8cef3e6c..47ae6802 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -9,7 +9,7 @@ class DeclareTest extends EmittingTest { public function no_strict_types() { Assert::equals(1, $this->run('class { public static function number(int $n) { return $n; } - public function run() { return self::number(1.5); } + public function run() { return self::number("1"); } }')); } @@ -17,15 +17,15 @@ public function run() { return self::number(1.5); } public function strict_types_off() { Assert::equals(1, $this->run('declare(strict_types = 0); class { public static function number(int $n) { return $n; } - public function run() { return self::number(1.5); } + public function run() { return self::number("1"); } }')); } - #[Test, Expect(class: Error::class, withMessage: '/must be of (the )?type int(eger)?, float given/')] + #[Test, Expect(class: Error::class, withMessage: '/must be of (the )?type int(eger)?, string given/')] public function strict_types_on() { $this->run('declare(strict_types = 1); class { public static function number(int $n) { return $n; } - public function run() { return self::number(1.5); } + public function run() { return self::number("1"); } }'); } } From d33f710d8ab0ea328695232fdb45b39d466bb84f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 21:47:24 +0200 Subject: [PATCH 439/926] Add initial support for callable syntax --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 7 + .../emit/CallableSyntaxTest.class.php | 139 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php diff --git a/composer.json b/composer.json index eada459d..1c12a90f 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.3", + "xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 02207b71..3a10a602 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -988,6 +988,13 @@ protected function emitNewClass($result, $new) { array_shift($result->type); } + protected function emitCallable($result, $callable) { + $t= $result->temp(); + $result->out->write('fn(...'.$t.') => '); + $this->emitOne($result, $callable->expression); + $result->out->write('(... '.$t.')'); + } + protected function emitInvoke($result, $invoke) { $this->emitOne($result, $invoke->expression); $result->out->write('('); diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php new file mode 100755 index 00000000..3e413ea7 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -0,0 +1,139 @@ +run('class { + public function run() { return strlen(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function class_method() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return self::length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function private_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_function() { + $f= $this->run('class { + public function run() { + $func= "strlen"; + return $func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method_reference() { + $f= $this->run('class { + private $func= "strlen"; + public function run() { + return ($this->func)(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_instance_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return $this->$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return self::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method_with_variable_class() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + $class= __CLASS__; + return $class::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function string_function_reference() { + $f= $this->run('class { + public function run() { return "strlen"(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_instance_method_reference() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return [$this, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_class_method_reference() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return [self::class, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } +} \ No newline at end of file From 30de58474229883e1c722d959d03ab6960d2832e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:33:22 +0200 Subject: [PATCH 440/926] Emit callable expressions as regular `function` closures for PHP < 7.4 --- .../ast/emit/CallablesAsClosures.class.php | 33 +++++++++++++++++++ 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 +- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100755 src/main/php/lang/ast/emit/CallablesAsClosures.class.php diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php new file mode 100755 index 00000000..d6489d4a --- /dev/null +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -0,0 +1,33 @@ + use ($closure) + // $obj->method(...); => use ($obj) + // $obj->$method(...); => use ($obj, $method) + // ($obj->property)(...); => use ($obj) + // $class::$method(...); => use ($class, $method) + // [$obj, 'method'](...); => use ($obj) + // [Foo::class, $method](...); => use ($method) + $use= []; + foreach ($result->codegen->search($callable, 'variable') as $var) { + 'this' === $var->name || $use[$var->name]= true; + } + + $t= $result->temp(); + $result->out->write('function(...'.$t.')'); + $use && $result->out->write('use($'.implode(', $', array_keys($use)).')'); + $result->out->write('{ return '); + $this->emitOne($result, $callable->expression); + $result->out->write('(... '.$t.'); }'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 4032fa26..45c85319 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers; + use OmitPropertyTypes, OmitConstModifiers, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 2c9f4640..1148183d 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes; + use OmitPropertyTypes, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index d717e5f7..0ce0e1e1 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes; + use OmitPropertyTypes, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ From 82f572c4d34e0ae0de51facf8d24b4b4885ae6e4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:34:53 +0200 Subject: [PATCH 441/926] Revert "Emit callable expressions as regular `function` closures for PHP < 7.4" This reverts commit 30de58474229883e1c722d959d03ab6960d2832e. --- .../ast/emit/CallablesAsClosures.class.php | 33 ------------------- 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 +- 4 files changed, 3 insertions(+), 36 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/CallablesAsClosures.class.php diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php deleted file mode 100755 index d6489d4a..00000000 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ /dev/null @@ -1,33 +0,0 @@ - use ($closure) - // $obj->method(...); => use ($obj) - // $obj->$method(...); => use ($obj, $method) - // ($obj->property)(...); => use ($obj) - // $class::$method(...); => use ($class, $method) - // [$obj, 'method'](...); => use ($obj) - // [Foo::class, $method](...); => use ($method) - $use= []; - foreach ($result->codegen->search($callable, 'variable') as $var) { - 'this' === $var->name || $use[$var->name]= true; - } - - $t= $result->temp(); - $result->out->write('function(...'.$t.')'); - $use && $result->out->write('use($'.implode(', $', array_keys($use)).')'); - $result->out->write('{ return '); - $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.'); }'); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 45c85319..4032fa26 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers, CallablesAsClosures; + use OmitPropertyTypes, OmitConstModifiers; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 1148183d..2c9f4640 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes, CallablesAsClosures; + use OmitPropertyTypes; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 0ce0e1e1..d717e5f7 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes, CallablesAsClosures; + use OmitPropertyTypes; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ From 376323884db625cbb5e0eda05740ddb2d078c4fb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:35:09 +0200 Subject: [PATCH 442/926] Revert "Add initial support for callable syntax" This reverts commit d33f710d8ab0ea328695232fdb45b39d466bb84f. --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 7 - .../emit/CallableSyntaxTest.class.php | 139 ------------------ 3 files changed, 1 insertion(+), 147 deletions(-) delete mode 100755 src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php diff --git a/composer.json b/composer.json index 1c12a90f..eada459d 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0", + "xp-framework/ast": "^7.3", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3a10a602..02207b71 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -988,13 +988,6 @@ protected function emitNewClass($result, $new) { array_shift($result->type); } - protected function emitCallable($result, $callable) { - $t= $result->temp(); - $result->out->write('fn(...'.$t.') => '); - $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.')'); - } - protected function emitInvoke($result, $invoke) { $this->emitOne($result, $invoke->expression); $result->out->write('('); diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php deleted file mode 100755 index 3e413ea7..00000000 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ /dev/null @@ -1,139 +0,0 @@ -run('class { - public function run() { return strlen(...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function instance_method() { - $f= $this->run('class { - public function length($arg) { return strlen($arg); } - public function run() { return $this->length(...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function class_method() { - $f= $this->run('class { - public static function length($arg) { return strlen($arg); } - public function run() { return self::length(...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function private_method() { - $f= $this->run('class { - private function length($arg) { return strlen($arg); } - public function run() { return $this->length(...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function variable_function() { - $f= $this->run('class { - public function run() { - $func= "strlen"; - return $func(...); - } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function instance_method_reference() { - $f= $this->run('class { - private $func= "strlen"; - public function run() { - return ($this->func)(...); - } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function variable_instance_method() { - $f= $this->run('class { - private function length($arg) { return strlen($arg); } - public function run() { - $func= "length"; - return $this->$func(...); - } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function variable_class_method() { - $f= $this->run('class { - private static function length($arg) { return strlen($arg); } - public function run() { - $func= "length"; - return self::$func(...); - } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function variable_class_method_with_variable_class() { - $f= $this->run('class { - private static function length($arg) { return strlen($arg); } - public function run() { - $func= "length"; - $class= __CLASS__; - return $class::$func(...); - } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function string_function_reference() { - $f= $this->run('class { - public function run() { return "strlen"(...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function array_instance_method_reference() { - $f= $this->run('class { - public function length($arg) { return strlen($arg); } - public function run() { return [$this, "length"](...); } - }'); - - Assert::equals(4, $f('Test')); - } - - #[Test] - public function array_class_method_reference() { - $f= $this->run('class { - public static function length($arg) { return strlen($arg); } - public function run() { return [self::class, "length"](...); } - }'); - - Assert::equals(4, $f('Test')); - } -} \ No newline at end of file From 1fd3f5b841aefd11203f1b58aa92948fc8f319c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:41:31 +0200 Subject: [PATCH 443/926] Implement first-class callable syntax See https://wiki.php.net/rfc/first_class_callable_syntax --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 7 +++++++ 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 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index eada459d..1c12a90f 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.3", + "xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 02207b71..3a10a602 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -988,6 +988,13 @@ protected function emitNewClass($result, $new) { array_shift($result->type); } + protected function emitCallable($result, $callable) { + $t= $result->temp(); + $result->out->write('fn(...'.$t.') => '); + $this->emitOne($result, $callable->expression); + $result->out->write('(... '.$t.')'); + } + protected function emitInvoke($result, $invoke) { $this->emitOne($result, $invoke->expression); $result->out->write('('); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 4032fa26..45c85319 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers; + use OmitPropertyTypes, OmitConstModifiers, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 2c9f4640..1148183d 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes; + use OmitPropertyTypes, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index d717e5f7..0ce0e1e1 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes; + use OmitPropertyTypes, CallablesAsClosures; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; /** Sets up type => literal mappings */ From 29c2a1c428822bb5648b8794a749cf46f4d74b5f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:42:21 +0200 Subject: [PATCH 444/926] Emit callable expressions as regular `function` closures for PHP < 7.4 --- .../ast/emit/CallablesAsClosures.class.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 src/main/php/lang/ast/emit/CallablesAsClosures.class.php diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php new file mode 100755 index 00000000..90ffc338 --- /dev/null +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -0,0 +1,34 @@ + use ($closure) + // $obj->method(...); => use ($obj) + // $obj->$method(...); => use ($obj, $method) + // ($obj->property)(...); => use ($obj) + // $class::$method(...); => use ($class, $method) + // [$obj, 'method'](...); => use ($obj) + // [Foo::class, $method](...); => use ($method) + $use= []; + foreach ($result->codegen->search($callable, 'variable') as $var) { + 'this' === $var->name || $use[$var->name]= true; + } + + // Create closure + $t= $result->temp(); + $result->out->write('function(...'.$t.')'); + $use && $result->out->write('use($'.implode(', $', array_keys($use)).')'); + $result->out->write('{ return '); + $this->emitOne($result, $callable->expression); + $result->out->write('(... '.$t.'); }'); + } +} \ No newline at end of file From ab460eff5517025c2d24b8c401332bf93a715b47 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 22:42:34 +0200 Subject: [PATCH 445/926] Add tests for callable syntax --- .../emit/CallableSyntaxTest.class.php | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php new file mode 100755 index 00000000..3e413ea7 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -0,0 +1,139 @@ +run('class { + public function run() { return strlen(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function class_method() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return self::length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function private_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_function() { + $f= $this->run('class { + public function run() { + $func= "strlen"; + return $func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function instance_method_reference() { + $f= $this->run('class { + private $func= "strlen"; + public function run() { + return ($this->func)(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_instance_method() { + $f= $this->run('class { + private function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return $this->$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return self::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function variable_class_method_with_variable_class() { + $f= $this->run('class { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + $class= __CLASS__; + return $class::$func(...); + } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function string_function_reference() { + $f= $this->run('class { + public function run() { return "strlen"(...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_instance_method_reference() { + $f= $this->run('class { + public function length($arg) { return strlen($arg); } + public function run() { return [$this, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } + + #[Test] + public function array_class_method_reference() { + $f= $this->run('class { + public static function length($arg) { return strlen($arg); } + public function run() { return [self::class, "length"](...); } + }'); + + Assert::equals(4, $f('Test')); + } +} \ No newline at end of file From 6b85e46ebcf91f55b532860f64b21049689fe383 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 2 Jul 2021 23:08:45 +0200 Subject: [PATCH 446/926] Remove unused imports --- .../php/lang/ast/unittest/emit/CallableSyntaxTest.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 3e413ea7..e93022ba 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,7 +1,6 @@ Date: Sat, 3 Jul 2021 11:03:06 +0200 Subject: [PATCH 447/926] Add variations to variable method reference tests --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index e93022ba..f18bb60f 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,6 +1,6 @@ $func(...)', '$this->{$func}(...)'])] + public function variable_instance_method($expr) { $f= $this->run('class { private function length($arg) { return strlen($arg); } public function run() { $func= "length"; - return $this->$func(...); + return '.$expr.'; } }'); Assert::equals(4, $f('Test')); } - #[Test] - public function variable_class_method() { + #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] + public function variable_class_method($expr) { $f= $this->run('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; - return self::$func(...); + return '.$expr.'; } }'); From a9bc033dd8ce9a8e3dabe8b40fe605b3e1a2cdee Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Jul 2021 11:07:13 +0200 Subject: [PATCH 448/926] Extract assertion into helper method --- .../emit/CallableSyntaxTest.class.php | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index f18bb60f..a5275a1d 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -2,100 +2,100 @@ use unittest\{Assert, Test, Values}; +/** + * Tests for first-class callable syntax + * + * @see https://wiki.php.net/rfc/first_class_callable_syntax#proposal + */ class CallableSyntaxTest extends EmittingTest { + /** + * Verification helper + * + * @param string $code + * @return void + * @throws unittest.AssertionFailedError + */ + private function verify($code) { + Assert::equals(4, $this->run($code)('Test')); + } + #[Test] public function native_function() { - $f= $this->run('class { + $this->verify('class { public function run() { return strlen(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function instance_method() { - $f= $this->run('class { + $this->verify('class { public function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function class_method() { - $f= $this->run('class { + $this->verify('class { public static function length($arg) { return strlen($arg); } public function run() { return self::length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function private_method() { - $f= $this->run('class { + $this->verify('class { private function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function variable_function() { - $f= $this->run('class { + $this->verify('class { public function run() { $func= "strlen"; return $func(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function instance_method_reference() { - $f= $this->run('class { + $this->verify('class { private $func= "strlen"; public function run() { return ($this->func)(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])] public function variable_instance_method($expr) { - $f= $this->run('class { + $this->verify('class { private function length($arg) { return strlen($arg); } public function run() { $func= "length"; return '.$expr.'; } }'); - - Assert::equals(4, $f('Test')); } #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] public function variable_class_method($expr) { - $f= $this->run('class { + $this->verify('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; return '.$expr.'; } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function variable_class_method_with_variable_class() { - $f= $this->run('class { + $this->verify('class { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; @@ -103,36 +103,28 @@ public function run() { return $class::$func(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function string_function_reference() { - $f= $this->run('class { + $this->verify('class { public function run() { return "strlen"(...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function array_instance_method_reference() { - $f= $this->run('class { + $this->verify('class { public function length($arg) { return strlen($arg); } public function run() { return [$this, "length"](...); } }'); - - Assert::equals(4, $f('Test')); } #[Test] public function array_class_method_reference() { - $f= $this->run('class { + $this->verify('class { public static function length($arg) { return strlen($arg); } public function run() { return [self::class, "length"](...); } }'); - - Assert::equals(4, $f('Test')); } } \ No newline at end of file From 12e517bf602f1de306ecae5ec626feab966000b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 11:36:58 +0200 Subject: [PATCH 449/926] Make code consistent with other places in emitter --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 90ffc338..92acd613 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -20,8 +20,9 @@ protected function emitCallable($result, $callable) { // [Foo::class, $method](...); => use ($method) $use= []; foreach ($result->codegen->search($callable, 'variable') as $var) { - 'this' === $var->name || $use[$var->name]= true; + $use[$var->name]= true; } + unset($use['this']); // Create closure $t= $result->temp(); From 1c2279bae22104d5fc7d8c6b97cffdd9b4caef08 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 11:39:39 +0200 Subject: [PATCH 450/926] QA: Compress whitespace --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 4 ++-- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 92acd613..4302ba81 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -27,9 +27,9 @@ protected function emitCallable($result, $callable) { // Create closure $t= $result->temp(); $result->out->write('function(...'.$t.')'); - $use && $result->out->write('use($'.implode(', $', array_keys($use)).')'); + $use && $result->out->write('use($'.implode(',$', array_keys($use)).')'); $result->out->write('{ return '); $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.'); }'); + $result->out->write('(...'.$t.'); }'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3a10a602..05e0241b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -990,9 +990,9 @@ protected function emitNewClass($result, $new) { protected function emitCallable($result, $callable) { $t= $result->temp(); - $result->out->write('fn(...'.$t.') => '); + $result->out->write('fn(...'.$t.')=>'); $this->emitOne($result, $callable->expression); - $result->out->write('(... '.$t.')'); + $result->out->write('(...'.$t.')'); } protected function emitInvoke($result, $invoke) { From 0c84bef73ff9b34bbebcb02b2b9650c2bbc44124 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 12:14:45 +0200 Subject: [PATCH 451/926] Add changelog entry --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 0620677c..d9addf0c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #114: Implements first-class callable syntax: `strlen(...)` + now returns a closure which if invoked with a string argument, returns + its length. Includes support for static and instance methods as well as + indirect references like `$closure(...)` and `self::{$expression}(...)`, + see https://wiki.php.net/rfc/first_class_callable_syntax + (@thekid) + ## 6.5.0 / 2021-05-22 * Merged PR #111: Add support for directives using declare - @thekid From a3217345b791249e42b622076aec3333a8076253 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 13:08:44 +0200 Subject: [PATCH 452/926] Add test for `fn() => ...` style closures --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index a5275a1d..81889372 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -52,7 +52,7 @@ public function run() { return $this->length(...); } } #[Test] - public function variable_function() { + public function string_reference() { $this->verify('class { public function run() { $func= "strlen"; @@ -62,7 +62,17 @@ public function run() { } #[Test] - public function instance_method_reference() { + public function fn_reference() { + $this->verify('class { + public function run() { + $func= fn($arg) => strlen($arg); + return $func(...); + } + }'); + } + + #[Test] + public function instance_property_reference() { $this->verify('class { private $func= "strlen"; public function run() { From 8ef7349a1ba7ebafcf719299dbe9d7ba9d5d4fd9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 13:54:07 +0200 Subject: [PATCH 453/926] Ensure exceptions are raised for non-existant callables See https://github.com/xp-framework/compiler/pull/114#discussion_r663485667 --- .../ast/emit/CallablesAsClosures.class.php | 63 ++++++++++++------- src/main/php/lang/ast/emit/PHP.class.php | 4 +- src/main/php/lang/ast/emit/PHP70.class.php | 51 ++++++++++++++- src/main/php/lang/ast/emit/PHP74.class.php | 2 +- src/main/php/lang/ast/emit/PHP80.class.php | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- .../emit/CallableSyntaxTest.class.php | 14 ++++- 7 files changed, 108 insertions(+), 30 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 4302ba81..4ac1c2b4 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,35 +1,54 @@ out->write('\Closure::fromCallable('); + if ($callable->expression instanceof Literal) { - // Use variables in the following cases: - // - // $closure(...); => use ($closure) - // $obj->method(...); => use ($obj) - // $obj->$method(...); => use ($obj, $method) - // ($obj->property)(...); => use ($obj) - // $class::$method(...); => use ($class, $method) - // [$obj, 'method'](...); => use ($obj) - // [Foo::class, $method](...); => use ($method) - $use= []; - foreach ($result->codegen->search($callable, 'variable') as $var) { - $use[$var->name]= true; - } - unset($use['this']); + // Rewrite f() => "f" + $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); + } else if ($callable->expression instanceof InstanceExpression) { + + // Rewrite $this->f => [$this, "f"] + $result->out->write('['); + $this->emitOne($result, $callable->expression->expression); + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else if ($callable->expression instanceof ScopeExpression) { - // Create closure - $t= $result->temp(); - $result->out->write('function(...'.$t.')'); - $use && $result->out->write('use($'.implode(',$', array_keys($use)).')'); - $result->out->write('{ return '); - $this->emitOne($result, $callable->expression); - $result->out->write('(...'.$t.'); }'); + // Rewrite self::f => ["self", "f"] + $result->out->write('['); + if ($callable->expression->type instanceof Node) { + $this->emitOne($result, $callable->expression->type); + } else { + $result->out->write('"'.$callable->expression->type.'"'); + } + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else { + + // Emit other expressions as-is + $this->emitOne($result, $callable->expression); + } + $result->out->write(')'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 05e0241b..6e1cd84f 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -989,10 +989,8 @@ protected function emitNewClass($result, $new) { } protected function emitCallable($result, $callable) { - $t= $result->temp(); - $result->out->write('fn(...'.$t.')=>'); $this->emitOne($result, $callable->expression); - $result->out->write('(...'.$t.')'); + $result->out->write('(...)'); } protected function emitInvoke($result, $invoke) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 45c85319..70f71279 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,5 +1,7 @@ literal mappings */ @@ -26,4 +28,51 @@ public function __construct() { }, ]; } + + protected function emitCallable($result, $callable) { + $t= $result->temp(); + $result->out->write('(is_callable('.$t.'='); + if ($callable->expression instanceof Literal) { + + // Rewrite f() => "f" + $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); + } else if ($callable->expression instanceof InstanceExpression) { + + // Rewrite $this->f => [$this, "f"] + $result->out->write('['); + $this->emitOne($result, $callable->expression->expression); + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else if ($callable->expression instanceof ScopeExpression) { + + // Rewrite self::f => [self::class, "f"] + $result->out->write('['); + if ($callable->expression->type instanceof Node) { + $this->emitOne($result, $callable->expression->type); + } else { + $result->out->write($callable->expression->type.'::class'); + } + if ($callable->expression->member instanceof Literal) { + $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member); + } + $result->out->write(']'); + } else { + + // Emit other expressions as-is + $this->emitOne($result, $callable->expression); + } + + // Emit equivalent of Closure::fromCallable() which doesn't exist until PHP 7.1 + $a= $result->temp(); + $result->out->write(')?function(...'.$a.') use('.$t.') { return '.$t.'(...'.$a.'); }:'); + $result->out->write('(function() { throw new \Error("Given argument is not callable"); })())'); + } } \ 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 1e0918fa..e8336ff8 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index d8205039..d5eae2f8 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 6d8d5693..4b9882b8 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 81889372..39961358 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -1,6 +1,7 @@ nonexistant', 'self::nonexistant', '$nonexistant', '$null'])] + public function non_existant($expr) { + $this->run('class { + public function run() { + $null= null; + $nonexistant= "nonexistant"; + return '.$expr.'(...); + } + }'); + } } \ No newline at end of file From af3b21641e54be3d9960487464da9940c9385ecc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 18:40:03 +0200 Subject: [PATCH 454/926] Remove unused Compiled::url_stat() --- src/main/php/lang/ast/Compiled.class.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 5a868edf..3391fad4 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -55,12 +55,12 @@ public function write($bytes) { $this->compiled.= $bytes; } - /** @return void */ + /** @codeCoverageIgnore */ public function flush() { // NOOP } - /** @return void */ + /** @codeCoverageIgnore */ public function close() { // NOOP } @@ -77,12 +77,6 @@ public function stream_read($count) { return $chunk; } - /** @return [:var] */ - public function url_stat($path) { - $opened= substr($path, strpos($path, '://') + 3); - return ['size' => self::$source[$opened]->getResourceAsStream($opened)->size()]; - } - /** @return [:var] */ public function stream_stat() { return ['size' => strlen($this->compiled)]; From f7d2783ef293ed96f7ce9b4483a3f3e29d0ae3f8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 19:18:27 +0200 Subject: [PATCH 455/926] Add tests for various loading exceptions --- .../lang/ast/CompilingClassloader.class.php | 4 +- .../loader/CompilingClassLoaderTest.class.php | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 3417cbe3..6283a8ab 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -166,8 +166,8 @@ public function loadClass0($class) { try { include($this->version.'://'.$uri); } catch (ClassLoadingException $e) { - unset(\xp::$cl[$class]); - throw $e; + unset(\xp::$cl[$class]); // @codeCoverageIgnore + throw $e; // @codeCoverageIgnore } catch (\Throwable $e) { unset(\xp::$cl[$class]); throw new ClassFormatException('Compiler error: '.$e->getMessage(), $e); diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 0cde12f5..646aff00 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -2,7 +2,7 @@ use io\{File, FileUtil, Folder}; use lang\ast\CompilingClassLoader; -use lang\{ClassFormatException, ElementNotFoundException, ClassLoader, Environment}; +use lang\{ClassFormatException, ClassNotFoundException, ElementNotFoundException, ClassLoader, Environment}; use unittest\{Assert, Expect, Test, TestCase}; class CompilingClassLoaderTest { @@ -68,6 +68,14 @@ public function load_class() { })); } + #[Test] + public function compare() { + $cl= CompilingClassLoader::instanceFor(self::$runtime); + + Assert::equals(0, $cl->compareTo($cl), 'equals itself'); + Assert::equals(1, $cl->compareTo(null), 'does not equal null'); + } + #[Test] public function load_dependencies() { $source= [ @@ -103,7 +111,17 @@ public function load_uri() { #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "{", have "(end)"')] public function load_class_with_syntax_errors() { - $this->compile(['Errors' => "loadClass($types['Errors']); }); + $this->compile(['Errors' => "loadClass($types['Errors']); + }); + } + + #[Test, Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class ".+" not found/')] + public function load_class_with_non_existant_parent() { + $code= "compile(['Orphan' => $code], function($loader, $types) { + return $loader->loadClass($types['Orphan']); + }); } #[Test] @@ -138,6 +156,21 @@ public function does_not_provide_non_existant_package() { Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesPackage('notfound')); } + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_uri() { + CompilingClassLoader::instanceFor(self::$runtime)->loadUri('NotFound.php'); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_class() { + CompilingClassLoader::instanceFor(self::$runtime)->loadClass('NotFound'); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_class_bytes() { + CompilingClassLoader::instanceFor(self::$runtime)->loadClassBytes('NotFound'); + } + #[Test, Expect(ElementNotFoundException::class)] public function loading_non_existant_resource() { CompilingClassLoader::instanceFor(self::$runtime)->getResource('notfound.md'); From 91451f18aef7ddc6b933239e0f0253e8c0ae36ca Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 19:21:20 +0200 Subject: [PATCH 456/926] Add test for packageContents() method --- .../unittest/loader/CompilingClassLoaderTest.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 646aff00..fbb06863 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -76,6 +76,14 @@ public function compare() { Assert::equals(1, $cl->compareTo(null), 'does not equal null'); } + #[Test] + public function package_contents() { + $contents= $this->compile(['Tests' => 'packageContents(strstr($types['Tests'], '.', true)); + }); + Assert::equals(['Tests'.\xp::CLASS_FILE_EXT], $contents); + } + #[Test] public function load_dependencies() { $source= [ From 0d1446a32a427d11d185876c80149aa8d251f942 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 19:30:54 +0200 Subject: [PATCH 457/926] Add tests for lookup() --- .../lang/ast/unittest/ResultTest.class.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index e5e0c113..9a47509c 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,7 +1,10 @@ out->write('echo "Hello";'); Assert::equals('bytes()); } + + #[Test] + public function lookup_self() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], [], null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); + } + + #[Test] + public function lookup_parent() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], [], null, 1); + + Assert::equals(new Reflection(Value::class), $r->lookup('parent')); + } + + #[Test] + public function lookup_named() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], [], null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); + } + + #[Test] + public function lookup_value_interface() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + + Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); + } } \ No newline at end of file From 1ce3212db274358d0d8b4b5a8acd17775fbdc7d7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 20:57:42 +0200 Subject: [PATCH 458/926] Add test for RewriteClassOnObjects trait --- .../unittest/emit/EmitterTraitTest.class.php | 26 ++++++++++++++++ .../emit/RewriteClassOnObjectsTest.class.php | 30 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php new file mode 100755 index 00000000..722aca93 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php @@ -0,0 +1,26 @@ +type= $type; + + $this->emitter->emitOne($result, $node); + return $result->out->bytes(); + } + + /** @return lang.ast.Emitter */ + protected abstract function fixture(); + + #[Before] + public function emitter() { + $this->emitter= $this->fixture(); + } +} \ 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/RewriteClassOnObjectsTest.class.php new file mode 100755 index 00000000..c0e90e6e --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php @@ -0,0 +1,30 @@ +emit( + new ScopeExpression(new Variable('instance'), new Literal('class'))) + ); + } + + #[Test] + public function does_not_rewrite_type_literal() { + Assert::equals('self::class', $this->emit( + new ScopeExpression('self', new Literal('class')), + [new ClassDeclaration([], '\\T', null, [], [], [], null, 1)] + )); + } +} \ No newline at end of file From 3062dc052e8fcc4a6991d9a4ebbcd3f046edff3f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:09:40 +0200 Subject: [PATCH 459/926] Add tests for RewriteLambdaExpressions trait --- .../RewriteLambdaExpressionsTest.class.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php new file mode 100755 index 00000000..a7252ef2 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php @@ -0,0 +1,31 @@ +emit( + new LambdaExpression(new Signature([], null), new Literal('true')) + )); + } + + #[Test] + public function rewrites_fn_with_block_to_function() { + Assert::equals('function(){return false;}', $this->emit( + new LambdaExpression(new Signature([], null), new Block([ + new ReturnStatement(new Literal('false')) + ])) + )); + } +} \ No newline at end of file From cfd6b0dd38b9fdbe120f05b1be87730eedfec03d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:11:16 +0200 Subject: [PATCH 460/926] Make test work with both PHP 7 and PHP 8 --- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index fbb06863..39d5cca5 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -124,7 +124,7 @@ public function load_class_with_syntax_errors() { }); } - #[Test, Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class ".+" not found/')] + #[Test, Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class .+ not found/')] public function load_class_with_non_existant_parent() { $code= "compile(['Orphan' => $code], function($loader, $types) { From 5d76d73cefb9149c93b4c1577c89faf08f4d2b9c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:23:18 +0200 Subject: [PATCH 461/926] Add tests for RewriteMultiCatch trait --- .../emit/RewriteMultiCatchTest.class.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php new file mode 100755 index 00000000..38e482f4 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php @@ -0,0 +1,39 @@ +emit(new TryStatement([], [new CatchStatement([], 't', [])], null)) + ); + } + + #[Test] + public function rewrites_catch_without_variable() { + Assert::equals( + 'try {}catch(\\Throwable $_0) {}', + $this->emit(new TryStatement([], [new CatchStatement([], null, [])], null)) + ); + } + + #[Test] + public function rewrites_catch_with_multiple_types_using_goto() { + Assert::equals( + 'try {}catch(\\Exception $t) { goto c2427456839; }catch(\\Error $t) { c2427456839:}', + $this->emit(new TryStatement([], [new CatchStatement(['\\Exception', '\\Error'], 't', [])], null)) + ); + } +} \ No newline at end of file From a9df9074f1946687c70280a3f094ba6448a486ba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:47:47 +0200 Subject: [PATCH 462/926] Only run test with non-existant as of PHP 7.3, fatals before --- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 39d5cca5..22887f25 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -3,7 +3,8 @@ use io\{File, FileUtil, Folder}; use lang\ast\CompilingClassLoader; use lang\{ClassFormatException, ClassNotFoundException, ElementNotFoundException, ClassLoader, Environment}; -use unittest\{Assert, Expect, Test, TestCase}; +use unittest\actions\RuntimeVersion; +use unittest\{Action, Assert, Expect, Test, TestCase}; class CompilingClassLoaderTest { private static $runtime; @@ -124,7 +125,7 @@ public function load_class_with_syntax_errors() { }); } - #[Test, Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class .+ not found/')] + #[Test, Action(eval: 'new RuntimeVersion(">=7.3")'), Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class .+ not found/')] public function load_class_with_non_existant_parent() { $code= "compile(['Orphan' => $code], function($loader, $types) { From 42689d2e3aca2aaecc64ced788898305cb835779 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:48:09 +0200 Subject: [PATCH 463/926] Add tests for RewriteNullCoalesceAssignment trait --- .../RewriteNullCoalesceAssignment.class.php | 4 +-- ...ewriteNullCoalesceAssignmentTest.class.php | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php diff --git a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php index 95288555..c86f30bd 100755 --- a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php +++ b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php @@ -11,9 +11,9 @@ trait RewriteNullCoalesceAssignment { protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { $this->emitAssign($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->variable); $result->out->write('??'); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); $this->emitOne($result, $assignment->expression); } else { parent::emitAssignment($result, $assignment); diff --git a/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php new file mode 100755 index 00000000..62f05a78 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php @@ -0,0 +1,31 @@ +emit(new Assignment(new Variable('v'), '??=', new Literal('1'))) + ); + } + + #[Test] + public function does_not_rewrite_plus() { + Assert::equals( + '$v+=1', + $this->emit(new Assignment(new Variable('v'), '+=', new Literal('1'))) + ); + } +} \ No newline at end of file From c147459bbc22fb06d369de68e62e9412feb2d29b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 21:57:14 +0200 Subject: [PATCH 464/926] Add test for Omit* traits --- .../emit/OmitConstModifiersTest.class.php | 32 +++++++++++++++++++ .../ast/unittest/emit/OmitTypesTest.class.php | 19 +++++++++++ 2 files changed, 51 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php b/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php new file mode 100755 index 00000000..65d881e4 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php @@ -0,0 +1,32 @@ +emit(new Constant([], 'TEST', new IsLiteral('string'), new Literal('"test"'))) + ); + } + + #[Test] + public function omits_modifier() { + Assert::equals( + 'const TEST="test";', + $this->emit(new Constant(['private'], 'TEST', new IsLiteral('string'), new Literal('"test"'))) + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php new file mode 100755 index 00000000..26d45ba2 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php @@ -0,0 +1,19 @@ +propertyType(new IsLiteral('int'))); + } + + #[Test] + public function return_type() { + Assert::equals('', $this->returnType(new IsLiteral('int'))); + } +} \ No newline at end of file From 352eeb492cbbdb6ed832d409562a209aa356ffda Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 22:32:27 +0200 Subject: [PATCH 465/926] Add tests for Result::lookup() as well as its return values --- .../lang/ast/unittest/ResultTest.class.php | 10 ++++- .../unittest/emit/DeclarationTest.class.php | 33 +++++++++++++++ .../unittest/emit/ReflectionTest.class.php | 40 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 9a47509c..8bd4fa5e 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,11 +1,11 @@ lookup('\\lang\\Value')); } + + #[Test, Expect(ClassNotFoundException::class)] + public function lookup_non_existant() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + $r->lookup('\\NotFound'); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php new file mode 100755 index 00000000..db3e751e --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php @@ -0,0 +1,33 @@ +type= new ClassDeclaration([], '\\T', '\\lang\\Enum', [], [ + '$ONE' => new Property(['public', 'static'], 'ONE', null, null, [], null, 1) + ]); + } + + #[Test] + public function can_create() { + new Declaration($this->type, null); + } + + #[Test] + public function name() { + Assert::equals('T', (new Declaration($this->type, null))->name()); + } + + #[Test] + public function rewrites_unit_enums() { + $declaration= new Declaration($this->type, null); + Assert::true($declaration->rewriteEnumCase('ONE')); + Assert::false($declaration->rewriteEnumCase('EMPTY')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php new file mode 100755 index 00000000..56de3fed --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php @@ -0,0 +1,40 @@ +name()); + } + + #[Test] + public function rewrites_unit_enums() { + $t= ClassLoader::defineClass('ReflectionTestEnum', null, [\UnitEnum::class], '{ + public static $ONE; + + public static $EMPTY= null; + + static function __static() { + self::$ONE= new self(); + } + }'); + + $reflect= new Reflection($t->literal()); + Assert::true($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } +} \ No newline at end of file From 824e82a5e8dcabd7a86d77f7e43dab3fef16b9e9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jul 2021 22:46:59 +0200 Subject: [PATCH 466/926] Add more tests for various enum implementations and runtimes --- .../unittest/emit/ReflectionTest.class.php | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php index 56de3fed..eb279197 100755 --- a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php @@ -1,8 +1,9 @@ 'class', 'extends' => [Enum::class], 'implements' => [], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestXPEnum', $spec, '{ public static $ONE; public static $EMPTY= null; @@ -37,4 +39,36 @@ static function __static() { Assert::true($reflect->rewriteEnumCase('ONE')); Assert::false($reflect->rewriteEnumCase('EMPTY')); } + + #[Test, Action(eval: 'new RuntimeVersion("<8.1")')] + public function rewrites_simulated_unit_enums() { + $spec= ['kind' => 'class', 'extends' => null, 'implements' => [\UnitEnum::class], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestSimulatedEnum', $spec, '{ + public static $ONE; + + public static $EMPTY= null; + + static function __static() { + self::$ONE= new self(); + } + }'); + + $reflect= new Reflection($t->literal()); + Assert::true($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.1")')] + public function does_not_rewrite_native_enums() { + $spec= ['kind' => 'enum', 'extends' => null, 'implements' => [], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestNativeEnum', $spec, '{ + case ONE; + + const EMPTY= null; + }'); + + $reflect= new Reflection($t->literal()); + Assert::false($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } } \ No newline at end of file From 03a2d4ac54a19ec500d01c39fd83893de78b3dbe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jul 2021 21:48:04 +0200 Subject: [PATCH 467/926] Add tests for xp.compiler.FromFile / FromFilesIn classes --- .../ast/unittest/cli/FromFileTest.class.php | 46 +++++++++++++++ .../unittest/cli/FromFilesInTest.class.php | 56 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/cli/FromFileTest.class.php create mode 100755 src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php new file mode 100755 index 00000000..a358696b --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php @@ -0,0 +1,46 @@ +folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new FromFile($this->file); + } + + #[Test] + public function can_create_from_string() { + new FromFile($this->file->getURI()); + } + + #[Test] + public function iteration() { + $results= []; + foreach (new FromFile($this->file) as $path => $stream) { + $results[(string)$path]= get_class($stream); + } + + Assert::equals([$this->file->getFileName() => FileInputStream::class], $results); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php new file mode 100755 index 00000000..6cc7ef7c --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php @@ -0,0 +1,56 @@ +folder, 'A.php'); + $a->touch(); + yield [[$a->getFileName() => $a->in()]]; + + $b= new File($this->folder, 'B.php'); + $b->touch(); + yield [[$a->getFileName() => $a->in(), $b->getFileName() => $b->in()]]; + } + + #[Before] + public function folder() { + $this->folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new FromFilesIn($this->folder); + } + + #[Test] + public function can_create_from_string() { + new FromFilesIn($this->folder->getURI()); + } + + #[Test, Values('files')] + public function iteration($expected) { + $results= []; + foreach (new FromFilesIn($this->folder) as $path => $stream) { + $results[(string)$path]= $stream; + } + + Assert::equals($expected, $results); + } +} \ No newline at end of file From 3a0e1f828462eb04d64974d25e21b0098c696bcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jul 2021 21:53:19 +0200 Subject: [PATCH 468/926] Add tests for recursiveness of FromFilesIn --- .../lang/ast/unittest/cli/FromFileTest.class.php | 5 ++--- .../lang/ast/unittest/cli/FromFilesInTest.class.php | 13 +++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php index a358696b..9cbb344f 100755 --- a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php @@ -1,6 +1,5 @@ file) as $path => $stream) { - $results[(string)$path]= get_class($stream); + $results[(string)$path]= $stream; } - Assert::equals([$this->file->getFileName() => FileInputStream::class], $results); + Assert::equals([$this->file->getFileName() => $this->file->in()], $results); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php index 6cc7ef7c..375e450e 100755 --- a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php @@ -1,6 +1,5 @@ folder, 'A.php'); $a->touch(); - yield [[$a->getFileName() => $a->in()]]; + yield [['A.php' => $a->in()]]; $b= new File($this->folder, 'B.php'); $b->touch(); - yield [[$a->getFileName() => $a->in(), $b->getFileName() => $b->in()]]; + yield [['A.php' => $a->in(), 'B.php' => $b->in()]]; + + $child= new Folder($this->folder, 'c'); + $child->create(); + $c= new File($child, 'C.php'); + $c->touch(); + yield [['A.php' => $a->in(), 'B.php' => $b->in(), 'c/C.php' => $c->in()]]; } #[Before] @@ -48,7 +53,7 @@ public function can_create_from_string() { public function iteration($expected) { $results= []; foreach (new FromFilesIn($this->folder) as $path => $stream) { - $results[(string)$path]= $stream; + $results[$path->toString('/')]= $stream; } Assert::equals($expected, $results); From ae01be4504c464a22bf005318f8e164a45ba88e5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 7 Jul 2021 18:32:59 +0200 Subject: [PATCH 469/926] Fix tests by not comparing FileInputStream instances --- .../php/lang/ast/unittest/cli/FromFileTest.class.php | 5 +++-- .../php/lang/ast/unittest/cli/FromFilesInTest.class.php | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php index 9cbb344f..a358696b 100755 --- a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php @@ -1,5 +1,6 @@ file) as $path => $stream) { - $results[(string)$path]= $stream; + $results[(string)$path]= get_class($stream); } - Assert::equals([$this->file->getFileName() => $this->file->in()], $results); + Assert::equals([$this->file->getFileName() => FileInputStream::class], $results); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php index 375e450e..f9d56cb3 100755 --- a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php @@ -1,5 +1,6 @@ folder, 'A.php'); $a->touch(); - yield [['A.php' => $a->in()]]; + yield [['A.php' => FileInputStream::class]]; $b= new File($this->folder, 'B.php'); $b->touch(); - yield [['A.php' => $a->in(), 'B.php' => $b->in()]]; + yield [['A.php' => FileInputStream::class, 'B.php' => FileInputStream::class]]; $child= new Folder($this->folder, 'c'); $child->create(); $c= new File($child, 'C.php'); $c->touch(); - yield [['A.php' => $a->in(), 'B.php' => $b->in(), 'c/C.php' => $c->in()]]; + yield [['A.php' => FileInputStream::class, 'B.php' => FileInputStream::class, 'c/C.php' => FileInputStream::class]]; } #[Before] @@ -53,7 +54,7 @@ public function can_create_from_string() { public function iteration($expected) { $results= []; foreach (new FromFilesIn($this->folder) as $path => $stream) { - $results[$path->toString('/')]= $stream; + $results[$path->toString('/')]= get_class($stream); } Assert::equals($expected, $results); From 7caff90bbe8e5c9c2519c33ea2c360ab1fb8808e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 7 Jul 2021 18:37:52 +0200 Subject: [PATCH 470/926] Add test for FromInputs class --- .../ast/unittest/cli/FromInputsTest.class.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php new file mode 100755 index 00000000..8c3e09c8 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php @@ -0,0 +1,30 @@ + ConsoleInputStream::class]]; + yield [[__FILE__, '-'], [basename(__FILE__) => FileInputStream::class, '-' => ConsoleInputStream::class]]; + } + + #[Test] + public function can_create() { + new FromInputs([]); + } + + #[Test, Values('inputs')] + public function iteration($inputs, $expected) { + $results= []; + foreach (new FromInputs($inputs) as $path => $stream) { + $results[$path->toString('/')]= get_class($stream); + } + + Assert::equals($expected, $results); + } +} \ No newline at end of file From 0a6635862caafab1b65cf49ebc2380f9ce4cff90 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 7 Jul 2021 18:50:03 +0200 Subject: [PATCH 471/926] Add tests for Input class --- .../lang/ast/unittest/cli/InputTest.class.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/cli/InputTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php new file mode 100755 index 00000000..3b3d59a0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -0,0 +1,57 @@ +folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function from_stdin() { + Assert::equals(new FromStream(Console::$in->getStream(), '-'), Input::newInstance('-')); + } + + #[Test] + public function from_file() { + Assert::equals(new FromFile($this->file), Input::newInstance($this->file->getURI())); + } + + #[Test] + public function from_folder() { + Assert::equals(new FromFilesIn($this->folder), Input::newInstance($this->folder->getURI())); + } + + #[Test] + public function from_empty_array() { + Assert::equals(new FromInputs([]), Input::newInstance([])); + } + + #[Test] + public function from_array() { + $array= ['-', $this->file]; + Assert::equals(new FromInputs($array), Input::newInstance($array)); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function from_illegal_argument() { + Input::newInstance(null); + } +} \ No newline at end of file From b376047401f624f772c7280e3ebf52bc180ad4c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:03:21 +0200 Subject: [PATCH 472/926] Add tests for PHP 8.1 emitter --- .../ast/unittest/emit/EmittingTest.class.php | 14 +++- .../ast/unittest/emit/PHP81Test.class.php | 79 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/PHP81Test.class.php diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 068a34c8..ab507c5b 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -20,7 +20,7 @@ public function __construct($output= null) { $this->output= $output ? array_flip(explode(',', $output)) : []; $this->cl= DynamicClassLoader::instanceFor(self::class); $this->language= Language::named('PHP'); - $this->emitter= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); + $this->emitter= Emitter::forRuntime($this->runtime())->newInstance(); foreach ($this->language->extensions() as $extension) { $extension->setup($this->language, $this->emitter); } @@ -33,6 +33,9 @@ public function tearDown() { } } + /** @return string */ + protected function runtime() { return 'PHP.'.PHP_VERSION; } + /** * Register a transformation. Will take care of removing it on test shutdown. * @@ -44,6 +47,15 @@ protected function transform($type, $function) { $this->transformations[]= $this->emitter->transform($type, $function); } + protected function emit($code) { + $name= 'E'.(self::$id++); + $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); + + $out= new MemoryOutputStream(); + $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); + return $out->bytes(); + } + /** * Declare a type * diff --git a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php new file mode 100755 index 00000000..217b1d38 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php @@ -0,0 +1,79 @@ +emit('f(key: "value");')); + } + + #[Test] + public function named_arguments() { + Assert::equals('f(color:"green",price:12.50);', $this->emit('f(color: "green", price: 12.50);')); + } + + #[Test] + public function new_type() { + Assert::equals('new \\T();', $this->emit('new T();')); + } + + #[Test] + public function new_expression() { + Assert::equals('new ($this->class)();', $this->emit('new ($this->class)();')); + } + + #[Test] + public function throw_expression() { + Assert::equals('fn()=>throw new \\T();', $this->emit('fn() => throw new T();')); + } + + #[Test] + public function catch_without_variable() { + Assert::equals('try {}catch(\\T) {};', $this->emit('try { } catch (\\T) { }')); + } + + #[Test] + public function catch_without_types() { + Assert::equals('try {}catch(\\Throwable $e) {};', $this->emit('try { } catch ($e) { }')); + } + + #[Test] + public function multi_catch_without_variable() { + Assert::equals('try {}catch(\\A|\\B) {};', $this->emit('try { } catch (\\A | \\B) { }')); + } + + #[Test] + public function null_safe() { + Assert::equals('$person?->name;', $this->emit('$person?->name;')); + } + + #[Test] + public function null_safe_expression() { + Assert::equals('$person?->{$name};', $this->emit('$person?->{$name};')); + } + + #[Test] + public function match() { + Assert::equals('match (true) {};', $this->emit('match (true) { };')); + } + + #[Test] + public function match_without_expression() { + Assert::equals('match (true) {};', $this->emit('match { };')); + } + + #[Test] + public function match_with_case() { + Assert::equals('match ($v) {1=>true,};', $this->emit('match ($v) { 1 => true };')); + } + + #[Test] + public function match_with_case_and_default() { + Assert::equals('match ($v) {1=>true,default=>false};', $this->emit('match ($v) { 1 => true, default => false };')); + } +} \ No newline at end of file From 6b8737988446244ae2efec4a015feabfee500196 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:05:49 +0200 Subject: [PATCH 473/926] QA: Add apidocs, reorder --- .../ast/unittest/emit/EmittingTest.class.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index ab507c5b..318d3213 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -26,14 +26,11 @@ public function __construct($output= null) { } } - #[After] - public function tearDown() { - foreach ($this->transformations as $transformation) { - $this->emitter->remove($transformation); - } - } - - /** @return string */ + /** + * Returns runtime to use. Uses `PHP_VERSION` constant. + * + * @return string + */ protected function runtime() { return 'PHP.'.PHP_VERSION; } /** @@ -47,6 +44,12 @@ protected function transform($type, $function) { $this->transformations[]= $this->emitter->transform($type, $function); } + /** + * Parse and emit given code + * + * @param string $code + * @return string + */ protected function emit($code) { $name= 'E'.(self::$id++); $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); @@ -95,4 +98,11 @@ protected function type($code) { protected function run($code, ... $args) { return $this->type($code)->newInstance()->run(...$args); } + + #[After] + public function tearDown() { + foreach ($this->transformations as $transformation) { + $this->emitter->remove($transformation); + } + } } \ No newline at end of file From 8e85697b1f063a3bd13b0e8598e4575f36f44ba9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:14:46 +0200 Subject: [PATCH 474/926] Prevent "Passing null to parameter 1 ($filename) of type string is deprecated" --- src/test/php/lang/ast/unittest/cli/InputTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php index 3b3d59a0..e7c790a7 100755 --- a/src/test/php/lang/ast/unittest/cli/InputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -52,6 +52,6 @@ public function from_array() { #[Test, Expect(IllegalArgumentException::class)] public function from_illegal_argument() { - Input::newInstance(null); + Input::newInstance(''); } } \ No newline at end of file From 6beee0fb5d2fec07ced807018e21348c13a71ccd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:19:30 +0200 Subject: [PATCH 475/926] Resolve temporary directory using realpath() --- src/test/php/lang/ast/unittest/cli/InputTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php index e7c790a7..5101d41b 100755 --- a/src/test/php/lang/ast/unittest/cli/InputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -11,7 +11,7 @@ class InputTest { #[Before] public function folder() { - $this->folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); $this->folder->exists() && $this->folder->unlink(); $this->folder->create(); From 49eba7ec3bb277088b90d408c0f67244997b2480 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:32:17 +0200 Subject: [PATCH 476/926] Test emitting enums --- .../ast/unittest/emit/PHP81Test.class.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php index 217b1d38..bd31e397 100755 --- a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php +++ b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php @@ -1,12 +1,25 @@ restore= Type::$ENUMS; + Type::$ENUMS= true; + } + + #[After] + public function restore() { + Type::$ENUMS= $this->restore; + } + #[Test] public function named_argument() { Assert::equals('f(key:"value");', $this->emit('f(key: "value");')); @@ -17,6 +30,22 @@ public function named_arguments() { Assert::equals('f(color:"green",price:12.50);', $this->emit('f(color: "green", price: 12.50);')); } + #[Test] + public function unit_enum() { + Assert::equals( + 'enum OS{case WINDOWS;case UNIX;};', + preg_replace('/static function __init.+__init\(\);/', '}', $this->emit('enum OS { case WINDOWS; case UNIX; }')) + ); + } + + #[Test] + public function backed_enum() { + Assert::equals( + 'enum Suit:string{case Hearts="♥";};', + preg_replace('/static function __init.+__init\(\);/', '}', $this->emit('enum Suit : string { case Hearts= "♥"; }')) + ); + } + #[Test] public function new_type() { Assert::equals('new \\T();', $this->emit('new T();')); From fa57c1d78bb49127c79ceec970c0ae00d9839070 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:41:56 +0200 Subject: [PATCH 477/926] Refactor code to use RewriteEnums trait for PHP < 8.1 --- src/main/php/lang/ast/emit/PHP.class.php | 60 ++------------ 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 | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 45 ---------- .../php/lang/ast/emit/RewriteEnums.class.php | 82 +++++++++++++++++++ 8 files changed, 96 insertions(+), 101 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteEnums.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 02207b71..e9056ebf 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -364,7 +364,12 @@ protected function emitLambda($result, $lambda) { } protected function emitEnumCase($result, $case) { - $result->out->write('public static $'.$case->name.';'); + $result->out->write('case '.$case->name); + if ($case->expression) { + $result->out->write('='); + $this->emitOne($result, $case->expression); + } + $result->out->write(';'); } protected function emitEnum($result, $enum) { @@ -372,64 +377,17 @@ protected function emitEnum($result, $enum) { array_unshift($result->meta, []); $result->locals= [[], []]; - $result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); - $enum->implements && $result->out->write(', '.implode(', ', $enum->implements)); + $result->out->write('enum '.$this->declaration($enum->name)); + $enum->base && $result->out->write(':'.$enum->base); + $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); $result->out->write('{'); - $cases= []; foreach ($enum->body as $member) { - if ($member->is('enumcase')) $cases[]= $member; $this->emitOne($result, $member); } - // Constructors - if ($enum->base) { - $result->out->write('public $name, $value;'); - $result->out->write('private static $values= [];'); - $result->out->write('private function __construct($name, $value) { - $this->name= $name; - $this->value= $value; - self::$values[$value]= $this; - }'); - $result->out->write('public static function tryFrom($value) { - return self::$values[$value] ?? null; - }'); - $result->out->write('public static function from($value) { - if ($r= self::$values[$value] ?? null) return $r; - throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); - }'); - } else { - $result->out->write('public $name;'); - $result->out->write('private function __construct($name) { - $this->name= $name; - }'); - } - - // Prevent cloning enums - $result->out->write('public function __clone() { - throw new \Error("Trying to clone an uncloneable object of class ".self::class); - }'); - - // Enum cases - $result->out->write('public static function cases() { return ['); - foreach ($cases as $case) { - $result->out->write('self::$'.$case->name.', '); - } - $result->out->write(']; }'); - // Initializations $result->out->write('static function __init() {'); - if ($enum->base) { - foreach ($cases as $case) { - $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", '); - $this->emitOne($result, $case->expression); - $result->out->write(');'); - } - } else { - foreach ($cases as $case) { - $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); - } - } $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); $result->out->write('}} '.$enum->name.'::__init();'); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 4032fa26..abeb39cf 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -9,7 +9,7 @@ */ class PHP70 extends PHP { use OmitPropertyTypes, OmitConstModifiers; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 2c9f4640..52446c1e 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -9,7 +9,7 @@ */ class PHP71 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index d717e5f7..0a264943 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -9,7 +9,7 @@ */ class PHP72 extends PHP { use OmitPropertyTypes; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 1e0918fa..1a252e4d 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index d8205039..f114218c 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 6d8d5693..fd95d873 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -40,51 +40,6 @@ protected function emitArguments($result, $arguments) { } } - protected function emitEnumCase($result, $case) { - - // TODO: Once enum PR is merged, remove this conditional and refactor the - // code into a `RewriteEnums` trait to be included for all other versions - if (Type::$ENUMS) { - $result->out->write('case '.$case->name); - if ($case->expression) { - $result->out->write('='); - $this->emitOne($result, $case->expression); - } - $result->out->write(';'); - } else { - parent::emitEnumCase($result, $case); - } - } - - protected function emitEnum($result, $enum) { - - // TODO: Once enum PR is merged, remove this conditional and refactor the - // code into a `RewriteEnums` trait to be included for all other versions - if (Type::$ENUMS) { - array_unshift($result->type, $enum); - array_unshift($result->meta, []); - $result->locals= [[], []]; - - $result->out->write('enum '.$this->declaration($enum->name)); - $enum->base && $result->out->write(':'.$enum->base); - $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); - $result->out->write('{'); - - foreach ($enum->body as $member) { - $this->emitOne($result, $member); - } - - // Initializations - $result->out->write('static function __init() {'); - $this->emitInitializations($result, $result->locals[0]); - $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); - $result->out->write('}} '.$enum->name.'::__init();'); - array_shift($result->type); - } else { - parent::emitEnum($result, $enum); - } - } - protected function emitNew($result, $new) { if ($new->type instanceof Node) { $result->out->write('new ('); diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php new file mode 100755 index 00000000..713a5d61 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -0,0 +1,82 @@ +out->write('public static $'.$case->name.';'); + } + + protected function emitEnum($result, $enum) { + array_unshift($result->type, $enum); + array_unshift($result->meta, []); + $result->locals= [[], []]; + + $result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); + $enum->implements && $result->out->write(', '.implode(', ', $enum->implements)); + $result->out->write('{'); + + $cases= []; + foreach ($enum->body as $member) { + if ($member->is('enumcase')) $cases[]= $member; + $this->emitOne($result, $member); + } + + // Constructors + if ($enum->base) { + $result->out->write('public $name, $value;'); + $result->out->write('private static $values= [];'); + $result->out->write('private function __construct($name, $value) { + $this->name= $name; + $this->value= $value; + self::$values[$value]= $this; + }'); + $result->out->write('public static function tryFrom($value) { + return self::$values[$value] ?? null; + }'); + $result->out->write('public static function from($value) { + if ($r= self::$values[$value] ?? null) return $r; + throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + }'); + } else { + $result->out->write('public $name;'); + $result->out->write('private function __construct($name) { + $this->name= $name; + }'); + } + + // Prevent cloning enums + $result->out->write('public function __clone() { + throw new \Error("Trying to clone an uncloneable object of class ".self::class); + }'); + + // Enum cases + $result->out->write('public static function cases() { return ['); + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.', '); + } + $result->out->write(']; }'); + + // Initializations + $result->out->write('static function __init() {'); + if ($enum->base) { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", '); + $this->emitOne($result, $case->expression); + $result->out->write(');'); + } + } else { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + } + } + $this->emitInitializations($result, $result->locals[0]); + $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); + $result->out->write('}} '.$enum->name.'::__init();'); + array_shift($result->type); + } +} \ No newline at end of file From 64771b63ae96a85c60db67be7c9931f2761cd118 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jul 2021 22:44:37 +0200 Subject: [PATCH 478/926] Remove conditionals --- .../php/lang/ast/unittest/emit/PHP81Test.class.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php index bd31e397..b6efbafe 100755 --- a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php +++ b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php @@ -1,25 +1,13 @@ restore= Type::$ENUMS; - Type::$ENUMS= true; - } - - #[After] - public function restore() { - Type::$ENUMS= $this->restore; - } - #[Test] public function named_argument() { Assert::equals('f(key:"value");', $this->emit('f(key: "value");')); From 020110fc8444deb81cfd22756679057de190cd27 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Jul 2021 09:20:45 +0200 Subject: [PATCH 479/926] Add test for Output class --- .../ast/unittest/cli/OutputTest.class.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/cli/OutputTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php new file mode 100755 index 00000000..c24a8954 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php @@ -0,0 +1,54 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + + $this->archive= new File($this->folder, 'dist.xar'); + $this->archive->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function compile_only() { + Assert::equals(new CompileOnly(), Output::newInstance(null)); + } + + #[Test] + public function to_stdin() { + Assert::equals(new ToStream(Console::$out->getStream()), Output::newInstance('-')); + } + + #[Test] + public function to_file() { + Assert::equals(new ToFile($this->file), Output::newInstance($this->file->getURI())); + } + + #[Test] + public function to_archive() { + Assert::equals(new ToArchive($this->archive), Output::newInstance($this->archive->getURI())); + } + + #[Test] + public function to_folder() { + Assert::equals(new ToFolder($this->folder), Output::newInstance($this->folder->getURI())); + } +} \ No newline at end of file From 2d593cf5ac6c9fbb6a63950bd1f40b23a82c58cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 9 Jul 2021 09:27:17 +0200 Subject: [PATCH 480/926] Summarize QA commits --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 0620677c..17c24712 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Removed conditional checks for PHP 8.1 with native enum support, all + releases and builds available on CI systems now contain it. + (@thekid) +* Increased test coverage significantly, especially of the compiler CLI + (@thekid) + ## 6.5.0 / 2021-05-22 * Merged PR #111: Add support for directives using declare - @thekid From 5ef55fa845aa14f13bdb86a1b286bd0cc93cd471 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Jul 2021 14:00:26 +0200 Subject: [PATCH 481/926] Add tests for various output implementations --- .../ast/unittest/cli/ToArchiveTest.class.php | 46 +++++++++++++++++ .../ast/unittest/cli/ToFileTest.class.php | 45 ++++++++++++++++ .../ast/unittest/cli/ToFolderTest.class.php | 51 +++++++++++++++++++ .../ast/unittest/cli/ToStreamTest.class.php | 29 +++++++++++ 4 files changed, 171 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php create mode 100755 src/test/php/lang/ast/unittest/cli/ToFileTest.class.php create mode 100755 src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php create mode 100755 src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php new file mode 100755 index 00000000..c0d0d1a8 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php @@ -0,0 +1,46 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->archive= new File($this->folder, 'dist.xar'); + $this->archive->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new ToArchive($this->archive); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'archive); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new ArchiveClassLoader($this->archive->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php new file mode 100755 index 00000000..72be2ef4 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php @@ -0,0 +1,45 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->target= new File($this->folder, 'Test.class.php'); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new ToFile($this->target); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'target); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new FileSystemClassLoader($this->folder->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php new file mode 100755 index 00000000..0c6d3c61 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php @@ -0,0 +1,51 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new ToFolder($this->folder); + } + + #[Test] + public function dash_special_case() { + with ((new ToFolder($this->folder))->target('-'), function($out) { + $out->write('folder, 'out'.\xp::CLASS_FILE_EXT))->exists()); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'folder); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new FileSystemClassLoader($this->folder->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php new file mode 100755 index 00000000..587dd606 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php @@ -0,0 +1,29 @@ +target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, $out->bytes()); + } +} \ No newline at end of file From 588085112568ef43478384ac65bc918f5f035c7d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Jul 2021 14:03:22 +0200 Subject: [PATCH 482/926] Add tests for CompileOnly class --- .../unittest/cli/CompileOnlyTest.class.php | 23 +++++++++++++++++++ .../ast/unittest/cli/ToStreamTest.class.php | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php diff --git a/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php new file mode 100755 index 00000000..c62a8da3 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php @@ -0,0 +1,23 @@ +target('Test.php'), function($out) { + $out->write('flush(); + $out->close(); + }); + $fixture->close(); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php index 587dd606..093f792b 100755 --- a/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php @@ -1,7 +1,7 @@ Date: Sat, 10 Jul 2021 14:11:39 +0200 Subject: [PATCH 483/926] Use PHP::emitLambda() instead of inlining --- .../lang/ast/emit/RewriteBlockLambdaExpressions.class.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 008dd28d..750d8c4e 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -16,10 +16,7 @@ protected function emitLambda($result, $lambda) { $this->emitAll($result, $body->statements); }); } else { - $result->out->write('fn'); - $this->emitSignature($result, $lambda->signature); - $result->out->write('=>'); - $this->emitOne($result, $lambda->body); + parent::emitLambda($result, $lambda); } } } \ No newline at end of file From fad8cf5166af53769221400ed5e7b581ca8048cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Jul 2021 14:14:39 +0200 Subject: [PATCH 484/926] Ignore code coverage for CLI entry points --- src/main/php/xp/compiler/AstRunner.class.php | 3 +++ src/main/php/xp/compiler/CompileRunner.class.php | 1 + src/main/php/xp/compiler/Usage.class.php | 1 + 3 files changed, 5 insertions(+) diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index 00b738ab..b8b4177c 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -3,6 +3,7 @@ use lang\ast\{Emitter, Errors, Language, Node, Result, Tokens}; use util\Objects; use util\cmd\Console; + /** * Display AST * @@ -14,6 +15,8 @@ * ```sh * $ echo " Date: Sat, 10 Jul 2021 14:20:41 +0200 Subject: [PATCH 485/926] Release 6.6.0 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 17c24712..528fb13e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.6.0 / 2021-07-10 + +* Emit null-coalesce operator as `$a ?? $a= expression` instead of as + `$a= $a ?? expression`, saving one assignment operation for non-null + case. Applies to PHP 7.0, 7.1 and 7.2. + (@thekid) * Removed conditional checks for PHP 8.1 with native enum support, all releases and builds available on CI systems now contain it. (@thekid) From ad91d484c7a48d0230107d53c5aba45a2bf81a68 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 10 Jul 2021 14:22:07 +0200 Subject: [PATCH 486/926] State actual code coverage facts [skip ci] --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 528fb13e..dc57fc52 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -12,7 +12,8 @@ XP Compiler ChangeLog * Removed conditional checks for PHP 8.1 with native enum support, all releases and builds available on CI systems now contain it. (@thekid) -* Increased test coverage significantly, especially of the compiler CLI +* Increased test coverage significantly (to more than 90%), especially + for classes used by the compiler command line. (@thekid) ## 6.5.0 / 2021-05-22 From f25b9d2f1ff7d0a378562b990fdab73a391960d2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:10:44 +0200 Subject: [PATCH 487/926] Use release version of xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1c12a90f..73d49ba9 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/first_class_callable_syntax as 7.4.0", + "xp-framework/ast": "^7.4", "php" : ">=7.0.0" }, "require-dev" : { From cf6f56a7e116741cf5815a785e71d607b5b09a45 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:18:30 +0200 Subject: [PATCH 488/926] Fix PHP 8.1 compatibility Calling static trait method T::__init is deprecated, it should only be called on a class using the trait --- src/main/php/lang/ast/emit/PHP.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d7d5621e..e20775b0 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -527,10 +527,9 @@ protected function emitTrait($result, $trait) { $this->emitOne($result, $member); $result->out->write("\n"); } + $result->out->write('}'); - $result->out->write('static function __init() {'); $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); - $result->out->write('}} '.$trait->name.'::__init();'); } protected function emitUse($result, $use) { From f8b9ecfd0f319a0d250ea559e9f1810f51315c2c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:21:37 +0200 Subject: [PATCH 489/926] Omit extra newlines when emitting members --- ChangeLog.md | 4 ++++ src/main/php/lang/ast/emit/PHP.class.php | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 81bf0309..72701c85 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,11 +3,15 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Changed emitter to omit extra newlines between members, making line + numbers more consistent with the original code. + (@thekid) * Merged PR #114: Implements first-class callable syntax: `strlen(...)` now returns a closure which if invoked with a string argument, returns its length. Includes support for static and instance methods as well as indirect references like `$closure(...)` and `self::{$expression}(...)`, see https://wiki.php.net/rfc/first_class_callable_syntax + (@thekid) ## 6.6.0 / 2021-07-10 diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index e20775b0..fb1693ae 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -511,7 +511,6 @@ protected function emitInterface($result, $interface) { $result->out->write('{'); foreach ($interface->body as $member) { $this->emitOne($result, $member); - $result->out->write("\n"); } $result->out->write('}'); @@ -525,7 +524,6 @@ protected function emitTrait($result, $trait) { $result->out->write('{'); foreach ($trait->body as $member) { $this->emitOne($result, $member); - $result->out->write("\n"); } $result->out->write('}'); @@ -936,7 +934,6 @@ protected function emitNewClass($result, $new) { $result->out->write('{'); foreach ($new->definition->body as $member) { $this->emitOne($result, $member); - $result->out->write("\n"); } $result->out->write('function __new() {'); $this->emitMeta($result, null, [], null); From 67072e87a263121f0defbdd47820688875267187 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 12 Jul 2021 21:36:18 +0200 Subject: [PATCH 490/926] Release 6.7.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 72701c85..eca9e1c6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.7.0 / 2021-07-12 + * Changed emitter to omit extra newlines between members, making line numbers more consistent with the original code. (@thekid) From f6e013cc8caba8ac799afb9fad9f007f2ec31325 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Jul 2021 16:27:00 +0200 Subject: [PATCH 491/926] Fix PHP 8.1 warnings about getIterator() signature compatibility --- src/main/php/xp/compiler/FromFile.class.php | 3 ++- src/main/php/xp/compiler/FromFilesIn.class.php | 3 ++- src/main/php/xp/compiler/FromInputs.class.php | 4 +++- src/main/php/xp/compiler/FromStream.class.php | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/php/xp/compiler/FromFile.class.php b/src/main/php/xp/compiler/FromFile.class.php index 19f90189..b33d5e0e 100755 --- a/src/main/php/xp/compiler/FromFile.class.php +++ b/src/main/php/xp/compiler/FromFile.class.php @@ -1,5 +1,6 @@ file->getFileName()) => $this->file->in(); } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/FromFilesIn.class.php b/src/main/php/xp/compiler/FromFilesIn.class.php index 7cd099c7..c16d4fd0 100755 --- a/src/main/php/xp/compiler/FromFilesIn.class.php +++ b/src/main/php/xp/compiler/FromFilesIn.class.php @@ -1,5 +1,6 @@ filesIn($this->folder) as $path => $stream) { yield $path => $stream; } diff --git a/src/main/php/xp/compiler/FromInputs.class.php b/src/main/php/xp/compiler/FromInputs.class.php index 3fb63459..78736f46 100755 --- a/src/main/php/xp/compiler/FromInputs.class.php +++ b/src/main/php/xp/compiler/FromInputs.class.php @@ -1,5 +1,7 @@ in as $in) { foreach (parent::newInstance($in) as $path => $stream) { yield $path => $stream; diff --git a/src/main/php/xp/compiler/FromStream.class.php b/src/main/php/xp/compiler/FromStream.class.php index 3d7d5545..6772e6f0 100755 --- a/src/main/php/xp/compiler/FromStream.class.php +++ b/src/main/php/xp/compiler/FromStream.class.php @@ -1,5 +1,6 @@ name) => $this->stream; } } \ No newline at end of file From 498aa6b519b24370f5574e85510b6c581a06315d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Jul 2021 19:06:47 +0200 Subject: [PATCH 492/926] QA: Fix return type --- src/main/php/lang/ast/Emitter.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index b29f2e18..cee628dd 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,8 +1,8 @@ Date: Sun, 25 Jul 2021 19:08:52 +0200 Subject: [PATCH 493/926] Use string interpolation instead of concatenation --- src/main/php/lang/ast/Emitter.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index cee628dd..ef9237fd 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -69,7 +69,7 @@ public function transformations() { } /** - * Catch-all, should `$node->kind` be empty in `{'emit'.$node->kind}`. + * Catch-all, should `$node->kind` be empty in `"emit{$node->kind}"`. * * @return void */ @@ -110,11 +110,11 @@ public function emitOne($result, $node) { $r= $transformation($result->codegen, $node); if ($r instanceof Node) { if ($r->kind === $node->kind) continue; - $this->{'emit'.$r->kind}($result, $r); + $this->{"emit{$r->kind}"}($result, $r); return; } else if ($r) { foreach ($r as $n) { - $this->{'emit'.$n->kind}($result, $n); + $this->{"emit{$n->kind}"}($result, $n); $result->out->write(';'); } return; From 37be0dc4d376537fb3ec8f8ba1e7042e5b7c580a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Aug 2021 13:20:15 +0200 Subject: [PATCH 494/926] Initial support for intersection types in PHP 8.1 Unchecked for all other PHP versions at the moment --- src/main/php/lang/ast/emit/PHP70.class.php | 17 ++++++------- src/main/php/lang/ast/emit/PHP71.class.php | 17 ++++++------- src/main/php/lang/ast/emit/PHP72.class.php | 17 ++++++------- src/main/php/lang/ast/emit/PHP74.class.php | 17 ++++++------- src/main/php/lang/ast/emit/PHP80.class.php | 17 ++++++------- src/main/php/lang/ast/emit/PHP81.class.php | 24 ++++++++++++------- .../emit/ControlStructuresTest.class.php | 2 +- 7 files changed, 62 insertions(+), 49 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 68d190d3..62f82166 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -2,7 +2,7 @@ use lang\ast\Node; use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal}; -use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral}; +use lang\ast\types\{IsUnion, IsIntersection, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral}; /** * PHP 7.0 syntax @@ -16,13 +16,14 @@ class PHP70 extends PHP { /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ - IsFunction::class => function($t) { return 'callable'; }, - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { return null; }, - IsUnion::class => function($t) { return null; }, - IsLiteral::class => function($t) { + IsFunction::class => function($t) { return 'callable'; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { return null; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { $l= $t->literal(); return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l || 'never' === $l) ? null : $l; }, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 7331a97f..659c2bb8 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -1,6 +1,6 @@ literal mappings */ public function __construct() { $this->literals= [ - IsFunction::class => function($t) { return 'callable'; }, - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsLiteral::class => function($t) { + IsFunction::class => function($t) { return 'callable'; }, + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { $l= $t->literal(); return ('object' === $l || 'mixed' === $l) ? null : ('never' === $l ? 'void' : $l); }, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 8e33fe69..84b9e3a3 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -1,6 +1,6 @@ literal mappings */ public function __construct() { $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsLiteral::class => function($t) { + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { $l= $t->literal(); return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); }, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 04f51259..efa3ddaf 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -1,6 +1,6 @@ literal mappings */ public function __construct() { $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsLiteral::class => function($t) { + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { $l= $t->literal(); return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); }, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 6c88ec1e..477ceb35 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,7 +1,7 @@ literal mappings */ public function __construct() { $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { return $t->literal(); }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsIntersection::class => function($t) { return null; }, + IsUnion::class => function($t) { $u= ''; foreach ($t->components as $component) { if (null === ($l= $this->literal($component))) return null; @@ -27,7 +28,7 @@ public function __construct() { } return substr($u, 1); }, - IsLiteral::class => function($t) { + IsLiteral::class => function($t) { $l= $t->literal(); return 'never' === $l ? 'void' : $l; } diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 0b287fc2..c04ec048 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -1,7 +1,7 @@ literal mappings */ public function __construct() { $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { return $t->literal(); }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { $u= ''; foreach ($t->components as $component) { if (null === ($l= $this->literal($component))) return null; @@ -27,7 +35,7 @@ public function __construct() { } return substr($u, 1); }, - IsLiteral::class => function($t) { return $t->literal(); } + IsLiteral::class => function($t) { return $t->literal(); } ]; } diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index fbd76d23..c91404cd 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -136,7 +136,7 @@ public function run($arg) { Assert::equals('10+ items', $r); } - #[Test, Expect(class: Throwable::class, withMessage: '/Unhandled match value of type .+/')] + #[Test, Expect(class: Throwable::class, withMessage: '/Unhandled match (value of type .+|case .+)/')] public function unhandled_match() { $this->run('class { public function run($arg) { From 138a8de10952deaaa11f0ac27fdf804481f88672 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 4 Aug 2021 11:34:38 +0200 Subject: [PATCH 495/926] Add tests using new XP reflection API functionality See xp-framework/core#277 --- .../emit/IntersectionTypesTest.class.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php new file mode 100755 index 00000000..13d24b79 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php @@ -0,0 +1,85 @@ +type('class { + private Traversable&Countable $test; + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getField('test')->getType() + ); + } + + #[Test] + public function parameter_type() { + $t= $this->type('class { + public function test(Traversable&Countable $arg) { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getMethod('test')->getParameter(0)->getType() + ); + } + + #[Test] + public function return_type() { + $t= $this->type('class { + public function test(): Traversable&Countable { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getMethod('test')->getReturnType() + ); + } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] + public function parameter_type_restriction_with_php81() { + $t= $this->type('class { + public function test(Traversable&Countable $arg) { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getMethod('test')->getParameter(0)->getTypeRestriction() + ); + } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] + public function parameter_function_type_restriction_with_php81() { + $t= $this->type('class { + public function test(): Traversable&Countable { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getMethod('test')->getReturnTypeRestriction() + ); + } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] + public function return_type_restriction_with_php81() { + $t= $this->type('class { + public function test(): Traversable&Countable { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->getMethod('test')->getReturnTypeRestriction() + ); + } +} \ No newline at end of file From 7a9ded415a1491bc76862a7de37f072c58f06b21 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 4 Aug 2021 11:40:54 +0200 Subject: [PATCH 496/926] QA: Rename test method for consistency --- .../php/lang/ast/unittest/emit/IntersectionTypesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php index 13d24b79..401141e4 100755 --- a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php @@ -60,7 +60,7 @@ public function test(Traversable&Countable $arg) { } } #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] - public function parameter_function_type_restriction_with_php81() { + public function parameter_type_restriction_with_php81() { $t= $this->type('class { public function test(): Traversable&Countable { } }'); From a4f6d33a6e83309286a177235a41d478bdad69fd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 4 Aug 2021 11:50:32 +0200 Subject: [PATCH 497/926] Verify field types with PHP 8.1 --- .../ast/unittest/emit/IntersectionTypesTest.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php index 401141e4..7c8f7695 100755 --- a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php @@ -48,26 +48,26 @@ public function test(): Traversable&Countable { } } #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] - public function parameter_type_restriction_with_php81() { + public function field_type_restriction_with_php81() { $t= $this->type('class { - public function test(Traversable&Countable $arg) { } + private Traversable&Countable $test; }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getParameter(0)->getTypeRestriction() + $t->getField('test')->getTypeRestriction() ); } #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] public function parameter_type_restriction_with_php81() { $t= $this->type('class { - public function test(): Traversable&Countable { } + public function test(Traversable&Countable $arg) { } }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getReturnTypeRestriction() + $t->getMethod('test')->getParameter(0)->getTypeRestriction() ); } From 59f1c2594c8d9d9538a497a5da9f03fb93b993d1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 4 Aug 2021 11:55:24 +0200 Subject: [PATCH 498/926] Release 6.8.0 --- ChangeLog.md | 6 ++++++ composer.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index eca9e1c6..9413cead 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.8.0 / 2021-08-04 + +* Merged PR #120: Support for PHP 8.1 intersection types, accessible via + reflection in all PHP versions, and runtime type-checked with 8.1.0+ + (@thekid) + ## 6.7.0 / 2021-07-12 * Changed emitter to omit extra newlines between members, making line diff --git a/composer.json b/composer.json index 73d49ba9..d052caf2 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.4", + "xp-framework/ast": "^7.5", "php" : ">=7.0.0" }, "require-dev" : { From 763ef9c0ad02cb93e2e6fb150bf2ef62aed17945 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 16 Aug 2021 21:21:11 +0200 Subject: [PATCH 499/926] Fix PHP 8.1 native enum constants in PHP 8.0 and lower Fixes issue #122 --- ChangeLog.md | 6 ++++++ src/main/php/lang/ast/CompilingClassloader.class.php | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 9413cead..4c795e1f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.8.1 / 2021-08-16 + +* Fixed issue #122: Undefined constant `Sources::FromEmpty` (when using + PHP 8.1 native enumerations in PHP 8.0 and lower *and* the JIT compiler) + (@thekid) + ## 6.8.0 / 2021-08-04 * Merged PR #120: Support for PHP 8.1 intersection types, accessible via diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 6283a8ab..9addbe8a 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,5 +1,6 @@ version= $emit->getSimpleName(); From 49a204321238ae7a6d729895a40f17e12d761f28 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 16 Aug 2021 21:41:57 +0200 Subject: [PATCH 500/926] Link to XP compiler design wiki page [skip ci] --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 09a9b0b7..69b27390 100755 --- a/README.md +++ b/README.md @@ -103,5 +103,6 @@ To contribute, open issues and/or pull requests. See also -------- +* [XP Compiler design](https://github.com/xp-framework/compiler/wiki/Compiler-design) * [XP RFC #0299: Make XP compiler the TypeScript of PHP](https://github.com/xp-framework/rfc/issues/299) * [XP RFC #0327: Compile-time metaprogramming](https://github.com/xp-framework/rfc/issues/327) \ No newline at end of file From a6aa06f192de8d3c5b3e761617a67e6cde172cb3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 6 Sep 2021 22:10:40 +0200 Subject: [PATCH 501/926] Add switch_case_constant_ambiguity test --- .../emit/ControlStructuresTest.class.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index c91404cd..7a545c63 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -53,6 +53,24 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] + public function switch_case_constant_ambiguity($whence, $expected) { + $r= $this->run('class { + const SET = SEEK_SET; + const CURRENT = SEEK_CUR; + public function run($arg) { + $position= 1; + switch ($arg) { + case self::SET: $position= 10; break; + case self::CURRENT: $position+= 10; break; + } + return $position; + } + }', $whence); + + Assert::equals($expected, $r); + } + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items']])] public function match($input, $expected) { $r= $this->run('class { From 28bad66084b446be36985a9cf0c4719b808cf222 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 9 Sep 2021 20:35:21 +0200 Subject: [PATCH 502/926] Prevent "Call to undefined method ...::emitoperator()" errors --- src/main/php/lang/ast/Emitter.class.php | 13 ++++++++++- .../unittest/emit/SyntaxErrorsTest.class.php | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index ef9237fd..447dea1c 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,6 +1,6 @@ value.' at line '.$operator->line); + } + /** * Emit nodes seperated as statements * diff --git a/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php new file mode 100755 index 00000000..a9ec1735 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php @@ -0,0 +1,23 @@ +emit('$greeting= hello() world()'); + } + + #[Test, Expect(class: Errors::class, withMessage: 'Unexpected :')] + public function unexpected_colon() { + $this->emit('$greeting= hello();:'); + } + + #[Test, Expect(class: IllegalStateException::class, withMessage: 'Unexpected operator = at line 1')] + public function operator() { + $this->emit('=;'); + } +} From 85264a6040450a839af3dc2434eb6824eaf22c0b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 9 Sep 2021 20:36:31 +0200 Subject: [PATCH 503/926] Release 6.8.2 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 4c795e1f..e203e3c9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.8.2 / 2021-09-09 + +* Fixed *Call to undefined method ...::emitoperator())* which do not + provide any context as to where an extraneous operator was encountered. + (@thekid) + ## 6.8.1 / 2021-08-16 * Fixed issue #122: Undefined constant `Sources::FromEmpty` (when using From aa47d91cd16c9c49d0cd0def970be9a1d5d4501d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 14:52:59 +0200 Subject: [PATCH 504/926] Emit property types for promoted properties Fixes "Readonly property T1::$fixture must have type" errors --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index fb1693ae..586a5d9e 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -602,7 +602,7 @@ protected function emitMethod($result, $method) { $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; if (isset($param->promote)) { - $promoted.= $param->promote.' $'.$param->name.';'; + $promoted.= $param->promote.' '.$this->propertyType($param->type).' $'.$param->name.';'; $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); $result->meta[0][self::PROPERTY][$param->name]= [ DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', From ca5e5917627744b02577dd69337661eb705d75f5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 14:54:25 +0200 Subject: [PATCH 505/926] Release 6.8.3 See https://github.com/xp-framework/compiler/issues/115#issuecomment-917397238 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index e203e3c9..bfc9af6a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.8.3 / 2021-09-11 + +* Fixed types not being emitted for promoted properties - @thekid + ## 6.8.2 / 2021-09-09 * Fixed *Call to undefined method ...::emitoperator())* which do not From ad610a6c9b8f254d9d6911290993e40dcda31b1b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 19:27:29 +0200 Subject: [PATCH 506/926] Add support for readonly properties Use native `readonly` modifier in PHP 8.1, simulated via virtual properties for PHP 7.X and PHP 8.0, see https://github.com/xp-framework/compiler/issues/115 --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 66 +++++++++++++----- 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 | 2 +- .../ast/emit/ReadonlyProperties.class.php | 35 ++++++++++ .../emit/ReadonlyPropertiesTest.class.php | 69 +++++++++++++++++++ 9 files changed, 160 insertions(+), 22 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ReadonlyProperties.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php diff --git a/composer.json b/composer.json index d052caf2..ac8b0164 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", + "xp-framework/core": "dev-rfc/340 as 10.13.0", "xp-framework/ast": "^7.5", "php" : ">=7.0.0" }, diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index fb1693ae..873f626e 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ type, $class); array_unshift($result->meta, []); - $result->locals= [[], []]; + $result->locals= [[], [], []]; $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); $class->parent && $result->out->write(' extends '.$class->parent); @@ -407,6 +407,38 @@ protected function emitClass($result, $class) { $this->emitOne($result, $member); } + // Virtual properties support + if ($result->locals[2]) { + $result->out->write('private $__virtual= ['); + foreach ($result->locals[2] as $name => $_) { + $result->out->write("'{$name}' => null,"); + } + $result->out->write('];'); + $result->out->write('public function __get($name) { + if (!array_key_exists($name, $this->__virtual)) { + trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); + } + + return $this->__virtual[$name][0] ?? null; + }'); + $result->out->write('public function __set($name, $value) { + if (isset($this->__virtual[$name])) { + throw new \\Error("Cannot modify readonly property ".__CLASS__."::".$name); + } + + $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + $scope= $caller["class"] ?? null; + if (__CLASS__ !== $scope && \lang\VirtualProperty::class !== $scope) { + throw new \Error("Cannot initialize readonly property ".__CLASS__."::".$name." from ".($scope + ? "scope ".$scope + : "global scope" + )); + } + + $this->__virtual[$name]= [$value]; + }'); + } + // Create constructor for property initializations to non-static scalars // which have not already been emitted inside constructor if ($result->locals[1]) { @@ -496,7 +528,10 @@ protected function emitMeta($result, $name, $annotations, $comment) { $this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]); $result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); - $result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); + $result->out->write(', DETAIL_ARGUMENTS => ['.($meta[DETAIL_ARGUMENTS] + ? "'".implode("', '", $meta[DETAIL_ARGUMENTS])."']]," + : ']],' + )); } $result->out->write('],'); } @@ -577,10 +612,10 @@ protected function emitMethod($result, $method) { // Include non-static initializations for members in constructor, removing // them to signal emitClass() no constructor needs to be generated. if ('__construct' === $method->name && isset($result->locals[1])) { - $locals= ['this' => true, 1 => $result->locals[1]]; + $locals= [[], $result->locals[1], [], 'this' => true]; $result->locals[1]= []; } else { - $locals= ['this' => true, 1 => []]; + $locals= [[], [], [], 'this' => true]; } $result->stack[]= $result->locals; $result->locals= $locals; @@ -596,21 +631,16 @@ protected function emitMethod($result, $method) { $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); $this->emitSignature($result, $method->signature); - $promoted= ''; + $promoted= []; foreach ($method->signature->parameters as $param) { $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + // Create properties from promoted parameters. Do not include default value, this is handled + // in emitParameter() already; otherwise we would be emitting it twice. if (isset($param->promote)) { - $promoted.= $param->promote.' $'.$param->name.';'; + $promoted[]= new Property(explode(' ', $param->promote), $param->name, $param->type, null, [], null, $param->line); $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); - $result->meta[0][self::PROPERTY][$param->name]= [ - DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', - DETAIL_ANNOTATIONS => [], - DETAIL_COMMENT => null, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; } if (isset($param->default) && !$this->isConstant($result, $param->default)) { @@ -627,9 +657,13 @@ protected function emitMethod($result, $method) { $result->out->write('}'); } - $result->out->write($promoted); + foreach ($promoted as $property) { + $this->emitOne($result, $property); + } + + // Copy any virtual properties inside locals[2] to class scope + $result->locals= [2 => $result->locals[2]] + array_pop($result->stack); $result->meta[0][self::METHOD][$method->name]= $meta; - $result->locals= array_pop($result->stack); } protected function emitBraced($result, $braced) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 62f82166..616017ca 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers; + use OmitPropertyTypes, OmitConstModifiers, ReadonlyProperties; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 659c2bb8..ad7ff0bf 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes, CallablesAsClosures; + use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 84b9e3a3..baad91de 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes, CallablesAsClosures; + use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties; use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index efa3ddaf..ca5fa5dd 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures, ReadonlyProperties; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 477ceb35..64f575cc 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php new file mode 100755 index 00000000..12ee3f59 --- /dev/null +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -0,0 +1,35 @@ +modifiers); + if (false === $p) return parent::emitProperty($result, $property); + + $result->meta[0][self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [MODIFIER_READONLY] + ]; + $result->locals[2][$property->name]= null; + + if (isset($property->expression)) { + if ($this->isConstant($result, $property->expression)) { + $result->out->write('='); + $this->emitOne($result, $property->expression); + } else if (in_array('static', $property->modifiers)) { + $result->locals[0]['self::$'.$property->name]= $property->expression; + } else { + $result->locals[1]['$this->'.$property->name]= $property->expression; + } + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php new file mode 100755 index 00000000..77af350d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php @@ -0,0 +1,69 @@ +type('class { + public readonly int $fixture; + }'); + + Assert::equals( + sprintf('public readonly int %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); + } + + #[Test] + public function with_constructor_argument_promotion() { + $t= $this->type('class { + public function __construct(public readonly string $fixture) { } + }'); + + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function reading() { + $t= $this->type('class { + public function __construct(public readonly string $fixture) { } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function assigning_inside_constructor() { + $t= $this->type('class { + public readonly string $fixture; + public function __construct($fixture) { $this->fixture= $fixture; } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function can_be_assigned_via_reflection() { + $t= $this->type('class { + public readonly string $fixture; + }'); + $i= $t->newInstance(); + $t->getField('fixture')->setAccessible(true)->set($i, 'Test'); + + Assert::equals('Test', $i->fixture); + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot modify readonly property .+fixture/')] + public function cannot_be_set_after_initialization() { + $t= $this->type('class { + public function __construct(public readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } +} \ No newline at end of file From fd9fe4712d1972db7c1de67bd1c8db36724a3e12 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 22:03:08 +0200 Subject: [PATCH 507/926] Add real virtual property support --- src/main/php/lang/ast/emit/PHP.class.php | 39 ++++++++----------- .../ast/emit/ReadonlyProperties.class.php | 22 ++++++++++- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 873f626e..9f59ebc6 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -407,36 +407,29 @@ protected function emitClass($result, $class) { $this->emitOne($result, $member); } - // Virtual properties support + // Virtual properties support: __virtual member + __get() and __set() if ($result->locals[2]) { $result->out->write('private $__virtual= ['); - foreach ($result->locals[2] as $name => $_) { + foreach ($result->locals[2] as $name => $access) { $result->out->write("'{$name}' => null,"); } $result->out->write('];'); - $result->out->write('public function __get($name) { - if (!array_key_exists($name, $this->__virtual)) { - trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); - } - - return $this->__virtual[$name][0] ?? null; - }'); - $result->out->write('public function __set($name, $value) { - if (isset($this->__virtual[$name])) { - throw new \\Error("Cannot modify readonly property ".__CLASS__."::".$name); - } - $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - $scope= $caller["class"] ?? null; - if (__CLASS__ !== $scope && \lang\VirtualProperty::class !== $scope) { - throw new \Error("Cannot initialize readonly property ".__CLASS__."::".$name." from ".($scope - ? "scope ".$scope - : "global scope" - )); - } + $result->out->write('public function __get($name) { switch ($name) {'); + foreach ($result->locals[2] as $name => $access) { + $result->out->write('case "'.$name.'":'); + $this->emitOne($result, $access[0]); + $result->out->write('break;'); + } + $result->out->write('default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); }}'); - $this->__virtual[$name]= [$value]; - }'); + $result->out->write('public function __set($name, $value) { switch ($name) {'); + foreach ($result->locals[2] as $name => $access) { + $result->out->write('case "'.$name.'":'); + $this->emitOne($result, $access[1]); + $result->out->write('break;'); + } + $result->out->write('}}'); } // Create constructor for property initializations to non-static scalars diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 12ee3f59..ae914a51 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -1,5 +1,7 @@ [], DETAIL_ARGUMENTS => [MODIFIER_READONLY] ]; - $result->locals[2][$property->name]= null; + + // Create virtual property + $result->locals[2][$property->name]= [ + new Code('return $this->__virtual[$name][0] ?? null;'), + new Code(' + if (isset($this->__virtual[$name])) { + throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}"); + } + $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + $scope= $caller["class"] ?? null; + if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope) { + throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope + ? "scope {$scope}" + : "global scope" + )); + } + $this->__virtual[$name]= [$value]; + '), + ]; if (isset($property->expression)) { if ($this->isConstant($result, $property->expression)) { From b8ca0a380892707eae92eb8e44265b566c55d519 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 22:10:10 +0200 Subject: [PATCH 508/926] Performance improvement for __get() and __set() implementations --- composer.json | 2 +- src/main/php/lang/ast/emit/ReadonlyProperties.class.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ac8b0164..d052caf2 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "dev-rfc/340 as 10.13.0", + "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/ast": "^7.5", "php" : ">=7.0.0" }, diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index ae914a51..52bcb7a6 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -22,11 +22,11 @@ protected function emitProperty($result, $property) { DETAIL_ARGUMENTS => [MODIFIER_READONLY] ]; - // Create virtual property + // Create virtual property implementing the readonly semantics $result->locals[2][$property->name]= [ - new Code('return $this->__virtual[$name][0] ?? null;'), + new Code('return $this->__virtual["'.$property->name.'"][0] ?? null;'), new Code(' - if (isset($this->__virtual[$name])) { + if (isset($this->__virtual["'.$property->name.'"])) { throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}"); } $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; @@ -37,7 +37,7 @@ protected function emitProperty($result, $property) { : "global scope" )); } - $this->__virtual[$name]= [$value]; + $this->__virtual["'.$property->name.'"]= [$value]; '), ]; From 62e870fa3fc4d71f04284f03e113956e353b448d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Sep 2021 22:59:08 +0200 Subject: [PATCH 509/926] Add actual modifiers to virtual properties --- .../ast/emit/ReadonlyProperties.class.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 52bcb7a6..268de61b 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -11,15 +11,28 @@ trait ReadonlyProperties { protected function emitProperty($result, $property) { - $p= array_search('readonly', $property->modifiers); - if (false === $p) return parent::emitProperty($result, $property); + static $lookup= [ + 'public' => MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY + ]; + + if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property); + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } $result->meta[0][self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [MODIFIER_READONLY] + DETAIL_ARGUMENTS => [$modifiers] ]; // Create virtual property implementing the readonly semantics From e8da9038fd044d35a05c14e808a6334e4257af04 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Sep 2021 00:44:53 +0200 Subject: [PATCH 510/926] Release 6.9.0 --- ChangeLog.md | 7 +++++++ composer.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index bfc9af6a..32a5df77 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.9.0 / 2021-09-12 + +* Merged PR #124: Add support for readonly properties. Implements feature + request #115, using native code for PHP 8.1 and simulated via virtual + properties for PHP 7.X and PHP 8.0 + (@thekid) + ## 6.8.3 / 2021-09-11 * Fixed types not being emitted for promoted properties - @thekid diff --git a/composer.json b/composer.json index d052caf2..ba2e5c18 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.5", + "xp-framework/ast": "^7.6", "php" : ">=7.0.0" }, "require-dev" : { From 79514a03132495fe4a5599c41d88962d0e85ce35 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Sep 2021 09:55:53 +0200 Subject: [PATCH 511/926] Use `php:X.Y` instead of `PHP.X.Y` for target runtimes Implements #123 --- ChangeLog.md | 6 ++++++ src/main/php/lang/ast/Emitter.class.php | 5 +++-- src/main/php/module.xp | 7 ++----- src/main/php/xp/compiler/AstRunner.class.php | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 4 ++-- src/test/php/lang/ast/unittest/EmitterTest.class.php | 9 +++++++-- .../php/lang/ast/unittest/emit/EmittingTest.class.php | 2 +- .../unittest/loader/CompilingClassLoaderTest.class.php | 8 ++++---- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 32a5df77..5b69c057 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.10.0 / 2021-09-12 + +* Implemented feature request #123: Use `php:X.Y` instead of *PHP.X.Y* + for target runtimes. The older syntax is still supported! + (@thekid) + ## 6.9.0 / 2021-09-12 * Merged PR #124: Add support for readonly properties. Implements feature diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 447dea1c..039aa282 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -10,14 +10,15 @@ abstract class Emitter { /** * Selects the correct emitter for a given runtime * - * @param string $runtime E.g. "PHP.".PHP_VERSION + * @param string $runtime E.g. "php:".PHP_VERSION * @return lang.XPClass * @throws lang.IllegalArgumentException */ public static function forRuntime($runtime) { - sscanf($runtime, '%[^.].%d.%d', $engine, $major, $minor); + sscanf($runtime, '%[^.:]%*[.:]%d.%d', $engine, $major, $minor); $p= Package::forName('lang.ast.emit'); + $engine= strtoupper($engine); do { $impl= $engine.$major.$minor; if ($p->providesClass($impl)) return $p->loadClass($impl); diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 96294d10..409bfef3 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -2,15 +2,12 @@ use lang\ClassLoader; -/** - * XP Compiler - */ +/** XP Compiler */ module xp-framework/compiler { /** @return void */ public function initialize() { - $runtime= 'PHP.'.PHP_VERSION; - ClassLoader::registerLoader(CompilingClassloader::instanceFor($runtime)); + ClassLoader::registerLoader(CompilingClassloader::instanceFor('php:'.PHP_VERSION)); if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index b8b4177c..223389ae 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -72,7 +72,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); + $emit= Emitter::forRuntime('php:'.PHP_VERSION)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 337ffc73..a9fe886f 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -32,7 +32,7 @@ * ``` * - Target PHP 7.4 (default target is current PHP version) * ```sh - * $ xp compile -t PHP.7.4 HelloWorld.php HelloWorld.class.php + * $ xp compile -t php:7.4 HelloWorld.php HelloWorld.class.php * ``` * * The *-o* and *-n* options accept multiple input sources following them. @@ -47,7 +47,7 @@ class CompileRunner { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $target= 'PHP.'.PHP_VERSION; + $target= 'php:'.PHP_VERSION; $in= $out= '-'; $quiet= false; for ($i= 0; $i < sizeof($args); $i++) { diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 39c75a4f..76844908 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -9,7 +9,7 @@ class EmitterTest { private function newEmitter() { - return Emitter::forRuntime('PHP.'.PHP_VERSION)->newInstance(); + return Emitter::forRuntime('php:'.PHP_VERSION)->newInstance(); } #[Test] @@ -17,9 +17,14 @@ public function can_create() { $this->newEmitter(); } + #[Test] + public function dotted_argument_bc() { + Assert::equals(Emitter::forRuntime('php:'.PHP_VERSION), Emitter::forRuntime('PHP.'.PHP_VERSION)); + } + #[Test, Expect(IllegalArgumentException::class)] public function cannot_create_for_unsupported_php_version() { - Emitter::forRuntime('PHP.4.3.0'); + Emitter::forRuntime('php:4.3.0'); } #[Test] diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 318d3213..6997ebdc 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -31,7 +31,7 @@ public function __construct($output= null) { * * @return string */ - protected function runtime() { return 'PHP.'.PHP_VERSION; } + protected function runtime() { return 'php:'.PHP_VERSION; } /** * Register a transformation. Will take care of removing it on test shutdown. diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 22887f25..8c6c8030 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -10,7 +10,7 @@ class CompilingClassLoaderTest { private static $runtime; static function __static() { - self::$runtime= 'PHP.'.PHP_VERSION; + self::$runtime= 'php:'.PHP_VERSION; } /** @@ -49,17 +49,17 @@ public function can_create() { #[Test, Values(['7.0.0', '7.0.1', '7.1.0', '7.2.0', '7.3.0', '7.4.0', '7.4.12', '8.0.0'])] public function supports_php($version) { - CompilingClassLoader::instanceFor('PHP.'.$version); + CompilingClassLoader::instanceFor('php:'.$version); } #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('PHP.7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); } #[Test] public function hashcode() { - Assert::equals('CPHP70', CompilingClassLoader::instanceFor('PHP.7.0.0')->hashCode()); + Assert::equals('CPHP70', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); } #[Test] From e8f9ba57cf220cee30e98ace3f96b2dd62b8a0fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Sep 2021 09:58:54 +0200 Subject: [PATCH 512/926] Update README with new target runtime syntax [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69b27390..e8322536 100755 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ $ xp compile -o dist.xar src/main/php/ $ xp compile -n src/main/php/ # Target PHP 7.4 (default target is current PHP version) -$ xp compile -t PHP.7.4 HelloWorld.php HelloWorld.class.php +$ xp compile -t php:7.4 HelloWorld.php HelloWorld.class.php ``` The -o and -n options accept multiple input sources following them. From 5fca2835f18a74bdfad0f6d099aaad359830196d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 26 Sep 2021 14:47:15 +0200 Subject: [PATCH 513/926] Add tests verifying global imports --- .../ast/unittest/emit/ImportTest.class.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index 4525174b..827eea6e 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -1,5 +1,6 @@ run('namespace test; + use Traversable; + + class { + public function run() { return Traversable::class; } + }' + )); + } + + #[Test] + public function import_globals_into_namespace() { + Assert::equals([Traversable::class, Iterator::class], $this->run('namespace test; + use Traversable, Iterator; + + class { + public function run() { return [Traversable::class, Iterator::class]; } + }' + )); + } } \ No newline at end of file From c5ba881532e1af402f2bcc7a3be0ea5e8de351bf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 27 Sep 2021 00:30:44 +0200 Subject: [PATCH 514/926] Check for PHP_VERSION_ID instead of whether `ReflectionEnum` exists --- src/main/php/lang/ast/emit/Type.class.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 211763d7..10b455e7 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -4,9 +4,7 @@ abstract class Type { public static $ENUMS; static function __static() { - - // TODO: Check PHP version ID once enum PR is merged - self::$ENUMS= class_exists(\ReflectionEnum::class, false); + self::$ENUMS= PHP_VERSION_ID >= 80100; } /** @return string */ From e4628dda1ecd144c18af330f60e2876d73a9614a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 27 Sep 2021 00:40:12 +0200 Subject: [PATCH 515/926] Add test for argument promotion with member initialization --- .../emit/InitializeWithExpressionsTest.class.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 6aefa700..961ebb86 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -204,4 +204,18 @@ public function run() { }'); Assert::equals(new Handle(0), $r); } + + #[Test] + public function with_argument_promotion() { + $t= $this->type('use lang\ast\unittest\emit\Handle; class { + private $h= new Handle(0); + + public function __construct(private Handle $p) { } + + public function run() { + return $this->h->compareTo($this->p); + } + }'); + Assert::equals(1, $t->newInstance(new Handle(1))->run()); + } } \ No newline at end of file From abaa2e6fea54298a90eb7e889c2a2f07e08357d9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 17:22:42 +0200 Subject: [PATCH 516/926] Support `new T(...)` as a shorthand for `fn(...$args) => new T(...$args)` See https://wiki.php.net/rfc/first_class_callable_syntax#object_creation --- .../php/lang/ast/emit/CallablesAsClosures.class.php | 8 +++++++- .../ast/unittest/emit/CallableSyntaxTest.class.php | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 4ac1c2b4..8c4e0afa 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,7 +1,7 @@ expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { + $result->out->write('fn(...$_args) => '); + $this->emitOne($result, $callable->expression); + return; + } + $result->out->write('\Closure::fromCallable('); if ($callable->expression instanceof Literal) { diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 39961358..ac81585f 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -149,4 +149,14 @@ public function run() { } }'); } + + #[Test] + public function instantiation() { + $f= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run() { + return new Handle(...); + } + }'); + Assert::equals(new Handle(1), $f(1)); + } } \ No newline at end of file From 64e86464964d28b23f8f4cb89f15c935325704c0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 17:33:24 +0200 Subject: [PATCH 517/926] Use arguments as declared by UnpackExpression --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 8c4e0afa..4053cafb 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,7 +1,7 @@ expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { - $result->out->write('fn(...$_args) => '); + $unpack= cast($callable->expression->arguments[0], UnpackExpression::class); + $result->out->write('fn(...$'.$unpack->expression->name.') => '); $this->emitOne($result, $callable->expression); return; } From b7ea3e74aa30e552d6474df1a40b74cb9465ef5a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 17:37:04 +0200 Subject: [PATCH 518/926] Add PHP 7.0, 7.1, 7.2 and 7.3 compatibility --- .../php/lang/ast/emit/CallablesAsClosures.class.php | 3 ++- src/main/php/lang/ast/emit/PHP70.class.php | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 4053cafb..2a828eab 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -13,8 +13,9 @@ trait CallablesAsClosures { protected function emitCallable($result, $callable) { if ($callable->expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { $unpack= cast($callable->expression->arguments[0], UnpackExpression::class); - $result->out->write('fn(...$'.$unpack->expression->name.') => '); + $result->out->write('function(...$'.$unpack->expression->name.') { return '); $this->emitOne($result, $callable->expression); + $result->out->write(';}'); return; } diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 616017ca..3abfcb26 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,7 +1,7 @@ expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { + $unpack= cast($callable->expression->arguments[0], UnpackExpression::class); + $result->out->write('function(...$'.$unpack->expression->name.') { return '); + $this->emitOne($result, $callable->expression); + $result->out->write(';}'); + return; + } + $t= $result->temp(); $result->out->write('(is_callable('.$t.'='); if ($callable->expression instanceof Literal) { From d32ee1a44b0aa7f35a623f4c0a1f9a1f1a0e6f9b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 21:52:31 +0200 Subject: [PATCH 519/926] Add tests for anonymous classes w/ callable `new` syntax --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index ac81585f..59b7e46b 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -159,4 +159,17 @@ public function run() { }'); Assert::equals(new Handle(1), $f(1)); } + + #[Test] + public function anonymous_instantiation() { + $f= $this->run('class { + public function run() { + return new class(...) { + public function __construct(private $value) { } + public function value() { return $this->value; } + }; + } + }'); + Assert::equals($this, $f($this)->value()); + } } \ No newline at end of file From 56c5d344083912121a2f144015fba0d3ea0d91fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 21:53:06 +0200 Subject: [PATCH 520/926] Use dedicated node introduced in xp-framework/ast@6df762b --- .../php/lang/ast/emit/CallablesAsClosures.class.php | 8 -------- src/main/php/lang/ast/emit/PHP.class.php | 13 ++++++++++++- src/main/php/lang/ast/emit/PHP70.class.php | 8 -------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 2a828eab..6ea72758 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -11,14 +11,6 @@ trait CallablesAsClosures { protected function emitCallable($result, $callable) { - if ($callable->expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { - $unpack= cast($callable->expression->arguments[0], UnpackExpression::class); - $result->out->write('function(...$'.$unpack->expression->name.') { return '); - $this->emitOne($result, $callable->expression); - $result->out->write(';}'); - return; - } - $result->out->write('\Closure::fromCallable('); if ($callable->expression instanceof Literal) { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9f59ebc6..0dd51dfa 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ out->write('(...)'); } + protected function emitCallableNew($result, $callable) { + $t= $result->temp(); + $result->out->write("function(...{$t}) { return "); + + $callable->type->arguments= [new UnpackExpression(new Variable(substr($t, 1)), $callable->line)]; + $this->emitOne($result, $callable->type); + $callable->type->arguments= null; + + $result->out->write("; }"); + } + protected function emitInvoke($result, $invoke) { $this->emitOne($result, $invoke->expression); $result->out->write('('); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 3abfcb26..8bdfbac7 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -31,14 +31,6 @@ public function __construct() { } protected function emitCallable($result, $callable) { - if ($callable->expression instanceof NewExpression || $callable->expression instanceof NewClassExpression) { - $unpack= cast($callable->expression->arguments[0], UnpackExpression::class); - $result->out->write('function(...$'.$unpack->expression->name.') { return '); - $this->emitOne($result, $callable->expression); - $result->out->write(';}'); - return; - } - $t= $result->temp(); $result->out->write('(is_callable('.$t.'='); if ($callable->expression instanceof Literal) { From 05ca30a9f571d2c032495d5560f4ca066d02868d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 21:53:53 +0200 Subject: [PATCH 521/926] Use feature/new-callable branch for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ba2e5c18..2d648ca6 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "^7.6", + "xp-framework/ast": "dev-feature/new-callable as 7.7.0", "php" : ">=7.0.0" }, "require-dev" : { From 6846adb10daf608ed42f4aa5b2c6c74113c7292b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 21:59:37 +0200 Subject: [PATCH 522/926] QA: Remove unused imports --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 2 +- src/main/php/lang/ast/emit/PHP70.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 6ea72758..4ac1c2b4 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,7 +1,7 @@ Date: Tue, 5 Oct 2021 22:01:09 +0200 Subject: [PATCH 523/926] Simplify anonymous_instantiation() test --- .../php/lang/ast/unittest/emit/CallableSyntaxTest.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 59b7e46b..b83195dc 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -165,11 +165,11 @@ public function anonymous_instantiation() { $f= $this->run('class { public function run() { return new class(...) { - public function __construct(private $value) { } - public function value() { return $this->value; } + public $value; + public function __construct($value) { $this->value= $value; } }; } }'); - Assert::equals($this, $f($this)->value()); + Assert::equals($this, $f($this)->value); } } \ No newline at end of file From 95c170b9326d113ac08d922991c2b57f983aa536 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 22:21:02 +0200 Subject: [PATCH 524/926] Test new T(...) inside array_map() --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index b83195dc..8ee9d80e 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -160,6 +160,16 @@ public function run() { Assert::equals(new Handle(1), $f(1)); } + #[Test] + public function instantiation_in_map() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + public function run() { + return array_map(new Handle(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + #[Test] public function anonymous_instantiation() { $f= $this->run('class { From 0581338cffd15a5bbda43387d092c07fc3130b4e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 5 Oct 2021 22:53:55 +0200 Subject: [PATCH 525/926] QA: Add apidoc link --- src/main/php/lang/ast/emit/CallablesAsClosures.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 4ac1c2b4..6fb7214f 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -6,6 +6,7 @@ /** * Rewrites callable expressions to `Callable::fromClosure()` * + * @see https://www.php.net/manual/de/closure.fromcallable.php * @see https://wiki.php.net/rfc/first_class_callable_syntax */ trait CallablesAsClosures { From 146241ae7b2cde6545aae7c99d7669cb30c03ad1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 6 Oct 2021 20:53:41 +0200 Subject: [PATCH 526/926] Use release version for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2d648ca6..8e462034 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/new-callable as 7.7.0", + "xp-framework/ast": "^7.7", "php" : ">=7.0.0" }, "require-dev" : { From 46e5751838dea0023239ce4677d3d89889963b83 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 6 Oct 2021 20:56:44 +0200 Subject: [PATCH 527/926] Release 6.11.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 5b69c057..0bb2314e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 6.11.0 / 2021-10-06 + +* Merged PR #125: Support `new T(...)` callable syntax - @thekid + ## 6.10.0 / 2021-09-12 * Implemented feature request #123: Use `php:X.Y` instead of *PHP.X.Y* From 71788db203f6d02ac4f15952cc9ca5ad89b6e74e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 21 Oct 2021 18:07:42 +0200 Subject: [PATCH 528/926] Drop support for XP < 9 See xp-framework/rfc#341 --- ChangeLog.md | 6 +++++ src/main/php/xp/compiler/Input.class.php | 2 +- src/main/php/xp/compiler/Output.class.php | 2 +- .../lang/ast/unittest/cli/InputTest.class.php | 2 +- .../ast/unittest/cli/OutputTest.class.php | 2 +- .../emit/TransformationsTest.class.php | 23 +++++++++++++++---- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0bb2314e..10341944 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 7.0.0 / 2021-10-21 + +* Made compatible with XP 11 - @thekid +* Implemented xp-framework/rfc#341, dropping compatibility with XP 9 + (@thekid) + ## 6.11.0 / 2021-10-06 * Merged PR #125: Support `new T(...)` callable syntax - @thekid diff --git a/src/main/php/xp/compiler/Input.class.php b/src/main/php/xp/compiler/Input.class.php index 0ceb4032..322887b6 100755 --- a/src/main/php/xp/compiler/Input.class.php +++ b/src/main/php/xp/compiler/Input.class.php @@ -14,7 +14,7 @@ abstract class Input implements \IteratorAggregate { */ public static function newInstance($arg) { if ('-' === $arg) { - return new FromStream(Console::$in->getStream(), '-'); + return new FromStream(Console::$in->stream(), '-'); } else if (is_array($arg)) { return new FromInputs($arg); } else if (is_file($arg)) { diff --git a/src/main/php/xp/compiler/Output.class.php b/src/main/php/xp/compiler/Output.class.php index 7a4c2e8d..82be96d8 100755 --- a/src/main/php/xp/compiler/Output.class.php +++ b/src/main/php/xp/compiler/Output.class.php @@ -14,7 +14,7 @@ public static function newInstance($arg) { if (null === $arg) { return new CompileOnly(); } else if ('-' === $arg) { - return new ToStream(Console::$out->getStream()); + return new ToStream(Console::$out->stream()); } else if (strstr($arg, '.php')) { return new ToFile($arg); } else if (strstr($arg, '.xar')) { diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php index 5101d41b..b59d0d74 100755 --- a/src/test/php/lang/ast/unittest/cli/InputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -26,7 +26,7 @@ public function cleanup() { #[Test] public function from_stdin() { - Assert::equals(new FromStream(Console::$in->getStream(), '-'), Input::newInstance('-')); + Assert::equals(new FromStream(Console::$in->stream(), '-'), Input::newInstance('-')); } #[Test] diff --git a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php index c24a8954..4821b25e 100755 --- a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php @@ -34,7 +34,7 @@ public function compile_only() { #[Test] public function to_stdin() { - Assert::equals(new ToStream(Console::$out->getStream()), Output::newInstance('-')); + Assert::equals(new ToStream(Console::$out->stream()), Output::newInstance('-')); } #[Test] diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index ea170a42..a55e33df 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -52,13 +52,18 @@ public function __construct(int $id) { public function generates_string_representation() { $t= $this->type('#[Repr] class { private int $id; + private string $name; - public function __construct(int $id) { + public function __construct(int $id, string $name) { $this->id= $id; + $this->name= $name; } }'); Assert::true($t->hasMethod('toString')); - Assert::equals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); + Assert::equals( + "T@[\n id => 1\n name => \"Test\"\n]", + $t->getMethod('toString')->invoke($t->newInstance(1, 'Test')) + ); } #[Test, Values([['id', 1], ['name', 'Test']])] @@ -80,12 +85,20 @@ public function __construct(int $id, string $name) { public function generates_both() { $t= $this->type('#[Repr, Getters] class { private int $id; + private string $name; - public function __construct(int $id) { + public function __construct(int $id, string $name) { $this->id= $id; + $this->name= $name; } }'); - Assert::equals(1, $t->getMethod('id')->invoke($t->newInstance(1))); - Assert::equals("T@[\n id => 1\n]", $t->getMethod('toString')->invoke($t->newInstance(1))); + + $instance= $t->newInstance(1, 'Test'); + Assert::equals(1, $t->getMethod('id')->invoke($instance)); + Assert::equals('Test', $t->getMethod('name')->invoke($instance)); + Assert::equals( + "T@[\n id => 1\n name => \"Test\"\n]", + $t->getMethod('toString')->invoke($instance) + ); } } \ No newline at end of file From e1aede06bac7be301ccf807f67ed733221047aa4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 21 Oct 2021 18:08:39 +0200 Subject: [PATCH 529/926] Remove XP 9, XP 8, XP 7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8e462034..acd8632e 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", + "xp-framework/core": "^11.0 | ^10.0", "xp-framework/ast": "^7.7", "php" : ">=7.0.0" }, From 0fb01ba80e753db6a2dd83020be85a67307d8678 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:49:05 +0100 Subject: [PATCH 530/926] Fix missing member declarations Cause warnings in PHP 8.2 --- src/main/php/lang/ast/Compiled.class.php | 1 + src/main/php/lang/ast/CompilingClassloader.class.php | 1 + src/main/php/xp/compiler/FromInputs.class.php | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 3391fad4..853a3b02 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -6,6 +6,7 @@ class Compiled implements OutputStream { public static $source= [], $emit= [], $lang= []; private $compiled= '', $offset= 0; + public $context; public static function bytes($version, $source, $file) { $stream= $source[1]->getResourceAsStream($file); diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 9addbe8a..1027374c 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -17,6 +17,7 @@ class CompilingClassLoader implements IClassLoader { private static $instance= []; private $version; + private $source= []; static function __static() { diff --git a/src/main/php/xp/compiler/FromInputs.class.php b/src/main/php/xp/compiler/FromInputs.class.php index 78736f46..8b01a703 100755 --- a/src/main/php/xp/compiler/FromInputs.class.php +++ b/src/main/php/xp/compiler/FromInputs.class.php @@ -4,7 +4,7 @@ /** Various inputs */ class FromInputs extends Input { - private $inputs; + private $in; /** * Creates a new instance From 824eb90c4c1ca8f6f6fb5e78e283661461005e84 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:50:15 +0100 Subject: [PATCH 531/926] Add PHP 8.2 emitter --- src/main/php/lang/ast/emit/PHP82.class.php | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 src/main/php/lang/ast/emit/PHP82.class.php diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php new file mode 100755 index 00000000..758140d9 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -0,0 +1,121 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); } + ]; + } + + protected function emitArguments($result, $arguments) { + $i= 0; + foreach ($arguments as $name => $argument) { + if ($i++) $result->out->write(','); + if (is_string($name)) $result->out->write($name.':'); + $this->emitOne($result, $argument); + } + } + + protected function emitNew($result, $new) { + if ($new->type instanceof Node) { + $result->out->write('new ('); + $this->emitOne($result, $new->type); + $result->out->write(')('); + } else { + $result->out->write('new '.$new->type.'('); + } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + + protected function emitThrowExpression($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + } + + protected function emitCatch($result, $catch) { + $capture= $catch->variable ? ' $'.$catch->variable : ''; + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable'.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + + protected function emitNullsafeInstance($result, $instance) { + $this->emitOne($result, $instance->expression); + $result->out->write('?->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + + protected function emitMatch($result, $match) { + if (null === $match->expression) { + $result->out->write('match (true) {'); + } else { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + } + + foreach ($match->cases as $case) { + $b= 0; + foreach ($case->expressions as $expression) { + $b && $result->out->write(','); + $this->emitOne($result, $expression); + $b++; + } + $result->out->write('=>'); + $this->emitAsExpression($result, $case->body); + $result->out->write(','); + } + + if ($match->default) { + $result->out->write('default=>'); + $this->emitAsExpression($result, $match->default); + } + + $result->out->write('}'); + } +} \ No newline at end of file From 4decfdf1f81ed3d2feca6604fe4c2b0ebf09c13c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:52:58 +0100 Subject: [PATCH 532/926] Add PHP 8.2 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8322536..54aae841 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack language, PHP 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack language, PHP 8.2, 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php From ec4f2d717a928740535289fd521723037c941e2b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:53:04 +0100 Subject: [PATCH 533/926] Release 7.1.0 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 10341944..5707e86b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 7.1.0 / 2021-12-08 + +* Added preliminary PHP 8.2 support by fixing various issues throughout + the code base and adding a PHP 8.2 emitter. + (@thekid) + ## 7.0.0 / 2021-10-21 * Made compatible with XP 11 - @thekid From 632c724d80b91a09f2852001f088b3cd383519b7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:53:37 +0100 Subject: [PATCH 534/926] Add PHP 8.2, upgrade XP runners --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef245d7f..fa3fb1bd 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions == '8.1' }} + continue-on-error: ${{ matrix.php-versions == '8.2' }} strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] os: [ubuntu-latest, windows-latest] steps: @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run && + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.6.1.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From 26ee5bab399053946df202703aeaa101a31b9384 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 20:58:13 +0100 Subject: [PATCH 535/926] Use io.Files instead of deprecated io.FileUtil --- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 8c6c8030..6ae71b2f 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -1,6 +1,6 @@ $code) { - FileUtil::setContents(new File($folder, $type.'.php'), sprintf($code, $namespace)); + Files::write(new File($folder, $type.'.php'), sprintf($code, $namespace)); $names[$type]= $namespace.'.'.$type; } $cl= ClassLoader::registerPath($folder->path); From 919c9b37514d8f66db5adb1a07fcd4ef02e56868 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 8 Dec 2021 23:39:49 +0100 Subject: [PATCH 536/926] Reduce code duplication --- .../ast/emit/CallablesAsClosures.class.php | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index 6fb7214f..e020b858 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -11,45 +11,41 @@ */ trait CallablesAsClosures { - protected function emitCallable($result, $callable) { - $result->out->write('\Closure::fromCallable('); - if ($callable->expression instanceof Literal) { + private function emitQuoted($result, $node) { + if ($node instanceof Literal) { // Rewrite f() => "f" - $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); - } else if ($callable->expression instanceof InstanceExpression) { + $result->out->write('"'.trim($node, '"\'').'"'); + } else if ($node instanceof InstanceExpression) { // Rewrite $this->f => [$this, "f"] $result->out->write('['); - $this->emitOne($result, $callable->expression->expression); - if ($callable->expression->member instanceof Literal) { - $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); - } else { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member); - } + $this->emitOne($result, $node->expression); + $result->out->write(','); + $this->emitQuoted($result, $node->member); $result->out->write(']'); - } else if ($callable->expression instanceof ScopeExpression) { + } else if ($node instanceof ScopeExpression) { - // Rewrite self::f => ["self", "f"] + // Rewrite T::f => [T::class, "f"] $result->out->write('['); - if ($callable->expression->type instanceof Node) { - $this->emitOne($result, $callable->expression->type); - } else { - $result->out->write('"'.$callable->expression->type.'"'); - } - if ($callable->expression->member instanceof Literal) { - $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + if ($node->type instanceof Node) { + $this->emitOne($result, $node->type); } else { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member); + $result->out->write($node->type.'::class'); } + $result->out->write(','); + $this->emitQuoted($result, $node->member); $result->out->write(']'); } else { // Emit other expressions as-is - $this->emitOne($result, $callable->expression); + $this->emitOne($result, $node); } + } + + protected function emitCallable($result, $callable) { + $result->out->write('\Closure::fromCallable('); + $this->emitQuoted($result, $callable->expression); $result->out->write(')'); } } \ No newline at end of file From 7cd28db31dc438da6585803dbd2b2e2e70eefd7d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 9 Dec 2021 00:11:32 +0100 Subject: [PATCH 537/926] Refactor all PHP 8.X syntax into base class... ...and extract rewrites into traits --- .../ast/emit/ArbitrayNewExpressions.class.php | 26 ++++++ .../lang/ast/emit/MatchAsTernaries.class.php | 41 ++++++++++ .../emit/NonCapturingCatchVariables.class.php | 20 +++++ .../ast/emit/NullsafeAsTernaries.class.php | 24 ++++++ .../lang/ast/emit/OmitArgumentNames.class.php | 17 ++++ src/main/php/lang/ast/emit/PHP.class.php | 61 ++++++-------- src/main/php/lang/ast/emit/PHP70.class.php | 4 +- src/main/php/lang/ast/emit/PHP71.class.php | 4 +- src/main/php/lang/ast/emit/PHP72.class.php | 4 +- src/main/php/lang/ast/emit/PHP74.class.php | 3 +- src/main/php/lang/ast/emit/PHP80.class.php | 80 ------------------- src/main/php/lang/ast/emit/PHP81.class.php | 80 ------------------- src/main/php/lang/ast/emit/PHP82.class.php | 80 ------------------- .../RewriteThrowableExpressions.class.php | 19 +++++ 14 files changed, 180 insertions(+), 283 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php create mode 100755 src/main/php/lang/ast/emit/MatchAsTernaries.class.php create mode 100755 src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php create mode 100755 src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php create mode 100755 src/main/php/lang/ast/emit/OmitArgumentNames.class.php create mode 100755 src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php diff --git a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php new file mode 100755 index 00000000..34424999 --- /dev/null +++ b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php @@ -0,0 +1,26 @@ +type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'= '); + $this->emitOne($result, $new->type); + $result->out->write(') ? new '.$t.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(') : null'); + } else { + $result->out->write('new '.$new->type.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/MatchAsTernaries.class.php b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php new file mode 100755 index 00000000..6d6da1df --- /dev/null +++ b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php @@ -0,0 +1,41 @@ +temp(); + if (null === $match->expression) { + $result->out->write('('.$t.'=true)'); + } else { + $result->out->write('('.$t.'='); + $this->emitOne($result, $match->expression); + $result->out->write(')'); + } + + $b= 0; + foreach ($match->cases as $case) { + foreach ($case->expressions as $expression) { + $b && $result->out->write($t); + $result->out->write('===('); + $this->emitOne($result, $expression); + $result->out->write(')?'); + $this->emitAsExpression($result, $case->body); + $result->out->write(':('); + $b++; + } + } + + // Emit IIFE for raising an error until we have throw expressions + if (null === $match->default) { + $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); + } else { + $this->emitAsExpression($result, $match->default); + } + $result->out->write(str_repeat(')', $b)); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php b/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php new file mode 100755 index 00000000..cd431929 --- /dev/null +++ b/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php @@ -0,0 +1,20 @@ +variable ? '$'.$catch->variable : $result->temp(); + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable '.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).' '.$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php new file mode 100755 index 00000000..3f6838c4 --- /dev/null +++ b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php @@ -0,0 +1,24 @@ +temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $instance->expression); + $result->out->write(')?null:'.$t.'->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitArgumentNames.class.php b/src/main/php/lang/ast/emit/OmitArgumentNames.class.php new file mode 100755 index 00000000..eb487440 --- /dev/null +++ b/src/main/php/lang/ast/emit/OmitArgumentNames.class.php @@ -0,0 +1,17 @@ +out->write(','); + $this->emitOne($result, $argument); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0dd51dfa..09c73e37 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -759,43 +759,40 @@ protected function emitSwitch($result, $switch) { } protected function emitMatch($result, $match) { - $t= $result->temp(); if (null === $match->expression) { - $result->out->write('('.$t.'=true)'); + $result->out->write('match (true) {'); } else { - $result->out->write('('.$t.'='); + $result->out->write('match ('); $this->emitOne($result, $match->expression); - $result->out->write(')'); + $result->out->write(') {'); } - $b= 0; foreach ($match->cases as $case) { + $b= 0; foreach ($case->expressions as $expression) { - $b && $result->out->write($t); - $result->out->write('===('); + $b && $result->out->write(','); $this->emitOne($result, $expression); - $result->out->write(')?'); - $this->emitAsExpression($result, $case->body); - $result->out->write(':('); $b++; } + $result->out->write('=>'); + $this->emitAsExpression($result, $case->body); + $result->out->write(','); } - // Emit IIFE for raising an error until we have throw expressions - if (null === $match->default) { - $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); - } else { + if ($match->default) { + $result->out->write('default=>'); $this->emitAsExpression($result, $match->default); } - $result->out->write(str_repeat(')', $b)); + + $result->out->write('}'); } protected function emitCatch($result, $catch) { - $capture= $catch->variable ? '$'.$catch->variable : $result->temp(); + $capture= $catch->variable ? ' $'.$catch->variable : ''; if (empty($catch->types)) { - $result->out->write('catch(\\Throwable '.$capture.') {'); + $result->out->write('catch(\\Throwable'.$capture.') {'); } else { - $result->out->write('catch('.implode('|', $catch->types).' '.$capture.') {'); + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); } $this->emitAll($result, $catch->body); $result->out->write('}'); @@ -824,13 +821,8 @@ protected function emitThrow($result, $throw) { } protected function emitThrowExpression($result, $throw) { - $result->out->write('('); - $this->enclose($result, $throw->expression, null, function($result, $expression) { - $result->out->write('throw '); - $this->emitOne($result, $expression); - $result->out->write(';'); - }); - $result->out->write(')()'); + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); } protected function emitForeach($result, $foreach) { @@ -920,25 +912,24 @@ protected function emitInstanceOf($result, $instanceof) { protected function emitArguments($result, $arguments) { $i= 0; - foreach ($arguments as $argument) { + foreach ($arguments as $name => $argument) { if ($i++) $result->out->write(','); + if (is_string($name)) $result->out->write($name.':'); $this->emitOne($result, $argument); } } protected function emitNew($result, $new) { if ($new->type instanceof Node) { - $t= $result->temp(); - $result->out->write('('.$t.'= '); + $result->out->write('new ('); $this->emitOne($result, $new->type); - $result->out->write(') ? new '.$t.'('); - $this->emitArguments($result, $new->arguments); - $result->out->write(') : null'); + $result->out->write(')('); } else { $result->out->write('new '.$new->type.'('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); } protected function emitNewClass($result, $new) { @@ -1032,10 +1023,8 @@ protected function emitInstance($result, $instance) { } protected function emitNullsafeInstance($result, $instance) { - $t= $result->temp(); - $result->out->write('null===('.$t.'='); $this->emitOne($result, $instance->expression); - $result->out->write(')?null:'.$t.'->'); + $result->out->write('?->'); if ('literal' === $instance->member->kind) { $result->out->write($instance->member->expression); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 616017ca..4f639c0b 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -10,8 +10,8 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers, ReadonlyProperties; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; + use OmitPropertyTypes, OmitConstModifiers, OmitArgumentNames, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, ArbitrayNewExpressions; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index ad7ff0bf..d42e3672 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,8 +8,8 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; + use OmitPropertyTypes, OmitArgumentNames, CallablesAsClosures, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index baad91de..377d1e7e 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,8 +8,8 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums; + use OmitPropertyTypes, OmitArgumentNames, CallablesAsClosures, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; + use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index ca5fa5dd..74acf541 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,7 +8,8 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures, ReadonlyProperties; + use CallablesAsClosures, OmitArgumentNames, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; + use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 64f575cc..f20f0d53 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -34,84 +34,4 @@ public function __construct() { } ]; } - - protected function emitArguments($result, $arguments) { - $i= 0; - foreach ($arguments as $name => $argument) { - if ($i++) $result->out->write(','); - if (is_string($name)) $result->out->write($name.':'); - $this->emitOne($result, $argument); - } - } - - protected function emitNew($result, $new) { - if ($new->type instanceof Node) { - $result->out->write('new ('); - $this->emitOne($result, $new->type); - $result->out->write(')('); - } else { - $result->out->write('new '.$new->type.'('); - } - - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - } - - protected function emitThrowExpression($result, $throw) { - $result->out->write('throw '); - $this->emitOne($result, $throw->expression); - } - - protected function emitCatch($result, $catch) { - $capture= $catch->variable ? ' $'.$catch->variable : ''; - if (empty($catch->types)) { - $result->out->write('catch(\\Throwable'.$capture.') {'); - } else { - $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); - } - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } - - protected function emitNullsafeInstance($result, $instance) { - $this->emitOne($result, $instance->expression); - $result->out->write('?->'); - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } - } - - protected function emitMatch($result, $match) { - if (null === $match->expression) { - $result->out->write('match (true) {'); - } else { - $result->out->write('match ('); - $this->emitOne($result, $match->expression); - $result->out->write(') {'); - } - - foreach ($match->cases as $case) { - $b= 0; - foreach ($case->expressions as $expression) { - $b && $result->out->write(','); - $this->emitOne($result, $expression); - $b++; - } - $result->out->write('=>'); - $this->emitAsExpression($result, $case->body); - $result->out->write(','); - } - - if ($match->default) { - $result->out->write('default=>'); - $this->emitAsExpression($result, $match->default); - } - - $result->out->write('}'); - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index c04ec048..b958f47c 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -38,84 +38,4 @@ public function __construct() { IsLiteral::class => function($t) { return $t->literal(); } ]; } - - protected function emitArguments($result, $arguments) { - $i= 0; - foreach ($arguments as $name => $argument) { - if ($i++) $result->out->write(','); - if (is_string($name)) $result->out->write($name.':'); - $this->emitOne($result, $argument); - } - } - - protected function emitNew($result, $new) { - if ($new->type instanceof Node) { - $result->out->write('new ('); - $this->emitOne($result, $new->type); - $result->out->write(')('); - } else { - $result->out->write('new '.$new->type.'('); - } - - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - } - - protected function emitThrowExpression($result, $throw) { - $result->out->write('throw '); - $this->emitOne($result, $throw->expression); - } - - protected function emitCatch($result, $catch) { - $capture= $catch->variable ? ' $'.$catch->variable : ''; - if (empty($catch->types)) { - $result->out->write('catch(\\Throwable'.$capture.') {'); - } else { - $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); - } - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } - - protected function emitNullsafeInstance($result, $instance) { - $this->emitOne($result, $instance->expression); - $result->out->write('?->'); - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } - } - - protected function emitMatch($result, $match) { - if (null === $match->expression) { - $result->out->write('match (true) {'); - } else { - $result->out->write('match ('); - $this->emitOne($result, $match->expression); - $result->out->write(') {'); - } - - foreach ($match->cases as $case) { - $b= 0; - foreach ($case->expressions as $expression) { - $b && $result->out->write(','); - $this->emitOne($result, $expression); - $b++; - } - $result->out->write('=>'); - $this->emitAsExpression($result, $case->body); - $result->out->write(','); - } - - if ($match->default) { - $result->out->write('default=>'); - $this->emitAsExpression($result, $match->default); - } - - $result->out->write('}'); - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 758140d9..12a4f63e 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -38,84 +38,4 @@ public function __construct() { IsLiteral::class => function($t) { return $t->literal(); } ]; } - - protected function emitArguments($result, $arguments) { - $i= 0; - foreach ($arguments as $name => $argument) { - if ($i++) $result->out->write(','); - if (is_string($name)) $result->out->write($name.':'); - $this->emitOne($result, $argument); - } - } - - protected function emitNew($result, $new) { - if ($new->type instanceof Node) { - $result->out->write('new ('); - $this->emitOne($result, $new->type); - $result->out->write(')('); - } else { - $result->out->write('new '.$new->type.'('); - } - - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); - } - - protected function emitThrowExpression($result, $throw) { - $result->out->write('throw '); - $this->emitOne($result, $throw->expression); - } - - protected function emitCatch($result, $catch) { - $capture= $catch->variable ? ' $'.$catch->variable : ''; - if (empty($catch->types)) { - $result->out->write('catch(\\Throwable'.$capture.') {'); - } else { - $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); - } - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } - - protected function emitNullsafeInstance($result, $instance) { - $this->emitOne($result, $instance->expression); - $result->out->write('?->'); - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } - } - - protected function emitMatch($result, $match) { - if (null === $match->expression) { - $result->out->write('match (true) {'); - } else { - $result->out->write('match ('); - $this->emitOne($result, $match->expression); - $result->out->write(') {'); - } - - foreach ($match->cases as $case) { - $b= 0; - foreach ($case->expressions as $expression) { - $b && $result->out->write(','); - $this->emitOne($result, $expression); - $b++; - } - $result->out->write('=>'); - $this->emitAsExpression($result, $case->body); - $result->out->write(','); - } - - if ($match->default) { - $result->out->write('default=>'); - $this->emitAsExpression($result, $match->default); - } - - $result->out->write('}'); - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php new file mode 100755 index 00000000..1d13a46f --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php @@ -0,0 +1,19 @@ +out->write('('); + $this->enclose($result, $throw->expression, null, function($result, $expression) { + $result->out->write('throw '); + $this->emitOne($result, $expression); + $result->out->write(';'); + }); + $result->out->write(')()'); + } +} \ No newline at end of file From d4adb1d97fc0b71d4e38c46e44548491ca96ad21 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 9 Dec 2021 12:34:02 +0100 Subject: [PATCH 538/926] QA: Add separator to help output --- src/main/php/xp/compiler/CompileRunner.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index a9fe886f..febc74ac 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -8,7 +8,8 @@ use util\profiling\Timer; /** - * Compiles future PHP to today's PHP. + * Compiles future PHP to today's PHP + * ================================== * * - Compile code and write result to a class file * ```sh From 494f84b16a9e393a86c27cc5e2c60afad4dbff8c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 10 Dec 2021 13:35:49 +0100 Subject: [PATCH 539/926] Test comments on enums --- src/test/php/lang/ast/unittest/emit/CommentsTest.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php index bc9b07ae..47df617e 100755 --- a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php @@ -22,6 +22,12 @@ public function on_trait() { Assert::equals('Test', $t->getComment()); } + #[Test] + public function on_enum() { + $t= $this->type('/** Test */ enum { }'); + Assert::equals('Test', $t->getComment()); + } + #[Test] public function on_method() { $t= $this->type('class { From 0d213f2f9a9ad650ebd343a1ef4e582d0fbcecaf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 10 Dec 2021 13:41:25 +0100 Subject: [PATCH 540/926] Add test for PHP 8.2 callable syntax --- src/main/php/lang/ast/emit/PHP81.class.php | 1 + src/main/php/lang/ast/emit/PHP82.class.php | 1 + .../ast/unittest/emit/PHP81Test.class.php | 2 +- .../ast/unittest/emit/PHP82Test.class.php | 25 +++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/emit/PHP82Test.class.php diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index b958f47c..35cb035d 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -6,6 +6,7 @@ /** * PHP 8.1 syntax * + * @test lang.ast.unittest.emit.PHP81Test * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 12a4f63e..df654511 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -6,6 +6,7 @@ /** * PHP 8.2 syntax * + * @test lang.ast.unittest.emit.PHP82Test * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { diff --git a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php index b6efbafe..b4932997 100755 --- a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php +++ b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php @@ -6,7 +6,7 @@ class PHP81Test extends EmittingTest { /** @return string */ - protected function runtime() { return 'PHP.8.1.0'; } + protected function runtime() { return 'php:8.1.0'; } #[Test] public function named_argument() { diff --git a/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php new file mode 100755 index 00000000..e9b1a8be --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php @@ -0,0 +1,25 @@ +emit('strlen(...);')); + } + + #[Test] + public function callable_static_method_syntax() { + Assert::equals('\lang\Type::forName(...);', $this->emit('\lang\Type::forName(...);')); + } + + #[Test] + public function callable_instance_method_syntax() { + Assert::equals('$this->method(...);', $this->emit('$this->method(...);')); + } +} \ No newline at end of file From 99281f97a8e635f68b054426c653e2dadf413d02 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Dec 2021 11:52:53 +0100 Subject: [PATCH 541/926] Emit unpack expressions inside arrays natively in PHP 8.1+ --- ChangeLog.md | 7 ++ .../ast/emit/ArrayUnpackUsingMerge.class.php | 65 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 53 ++------------- src/main/php/lang/ast/emit/PHP70.class.php | 19 +++++- src/main/php/lang/ast/emit/PHP71.class.php | 19 +++++- src/main/php/lang/ast/emit/PHP72.class.php | 19 +++++- src/main/php/lang/ast/emit/PHP74.class.php | 17 ++++- src/main/php/lang/ast/emit/PHP80.class.php | 2 +- 8 files changed, 146 insertions(+), 55 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 5707e86b..1c3d9c47 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 7.2.0 / 2021-12-20 + +* Optimized generated code for arrays including unpack expressions for + PHP 8.1+, which natively supports unpacking with keys. See + https://wiki.php.net/rfc/array_unpacking_string_keys + (@thekid) + ## 7.1.0 / 2021-12-08 * Added preliminary PHP 8.2 support by fixing various issues throughout diff --git a/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php new file mode 100755 index 00000000..d527b63f --- /dev/null +++ b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php @@ -0,0 +1,65 @@ +values)) { + $result->out->write('[]'); + return; + } + + $unpack= false; + foreach ($array->values as $pair) { + if ('unpack' === $pair[1]->kind) { + $unpack= true; + break; + } + } + + if ($unpack) { + $result->out->write('array_merge(['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + if ('unpack' === $pair[1]->kind) { + if ('array' === $pair[1]->expression->kind) { + $result->out->write('],'); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(',['); + } else { + $t= $result->temp(); + $result->out->write('],('.$t.'='); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); + } + } else { + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + } + $result->out->write('])'); + } else { + $result->out->write('['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(']'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 09c73e37..661fbff6 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -237,55 +237,16 @@ protected function emitCast($result, $cast) { } protected function emitArray($result, $array) { - if (empty($array->values)) { - $result->out->write('[]'); - return; - } - - $unpack= false; + $result->out->write('['); foreach ($array->values as $pair) { - if ('unpack' === $pair[1]->kind) { - $unpack= true; - break; - } - } - - if ($unpack) { - $result->out->write('array_merge(['); - foreach ($array->values as $pair) { - if ($pair[0]) { - $this->emitOne($result, $pair[0]); - $result->out->write('=>'); - } - if ('unpack' === $pair[1]->kind) { - if ('array' === $pair[1]->expression->kind) { - $result->out->write('],'); - $this->emitOne($result, $pair[1]->expression); - $result->out->write(',['); - } else { - $t= $result->temp(); - $result->out->write('],('.$t.'='); - $this->emitOne($result, $pair[1]->expression); - $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); - } - } else { - $this->emitOne($result, $pair[1]); - $result->out->write(','); - } + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); } - $result->out->write('])'); - } else { - $result->out->write('['); - foreach ($array->values as $pair) { - if ($pair[0]) { - $this->emitOne($result, $pair[0]); - $result->out->write('=>'); - } - $this->emitOne($result, $pair[1]); - $result->out->write(','); - } - $result->out->write(']'); + $this->emitOne($result, $pair[1]); + $result->out->write(','); } + $result->out->write(']'); } protected function emitParameter($result, $parameter) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 4f639c0b..596b033d 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -10,8 +10,23 @@ * @see https://wiki.php.net/rfc#php_70 */ class PHP70 extends PHP { - use OmitPropertyTypes, OmitConstModifiers, OmitArgumentNames, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, ArbitrayNewExpressions; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; + use + ArbitrayNewExpressions, + ArrayUnpackUsingMerge, + MatchAsTernaries, + NullsafeAsTernaries, + OmitArgumentNames, + OmitConstModifiers, + OmitPropertyTypes, + ReadonlyProperties, + RewriteClassOnObjects, + RewriteEnums, + RewriteExplicitOctals, + RewriteLambdaExpressions, + RewriteMultiCatch, + RewriteNullCoalesceAssignment, + RewriteThrowableExpressions + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index d42e3672..3fae3c60 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -8,8 +8,23 @@ * @see https://wiki.php.net/rfc#php_71 */ class PHP71 extends PHP { - use OmitPropertyTypes, OmitArgumentNames, CallablesAsClosures, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; + use + ArbitrayNewExpressions, + ArrayUnpackUsingMerge, + CallablesAsClosures, + MatchAsTernaries, + NonCapturingCatchVariables, + NullsafeAsTernaries, + OmitArgumentNames, + OmitPropertyTypes, + ReadonlyProperties, + RewriteClassOnObjects, + RewriteEnums, + RewriteExplicitOctals, + RewriteLambdaExpressions, + RewriteNullCoalesceAssignment, + RewriteThrowableExpressions + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 377d1e7e..bd6727dc 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -8,8 +8,23 @@ * @see https://wiki.php.net/rfc#php_72 */ class PHP72 extends PHP { - use OmitPropertyTypes, OmitArgumentNames, CallablesAsClosures, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; - use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; + use + ArbitrayNewExpressions, + ArrayUnpackUsingMerge, + CallablesAsClosures, + MatchAsTernaries, + NonCapturingCatchVariables, + NullsafeAsTernaries, + OmitArgumentNames, + OmitPropertyTypes, + ReadonlyProperties, + RewriteClassOnObjects, + RewriteEnums, + RewriteExplicitOctals, + RewriteLambdaExpressions, + RewriteNullCoalesceAssignment, + RewriteThrowableExpressions + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 74acf541..1d9c1d81 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -8,8 +8,21 @@ * @see https://wiki.php.net/rfc#php_74 */ class PHP74 extends PHP { - use CallablesAsClosures, OmitArgumentNames, ReadonlyProperties, MatchAsTernaries, NullsafeAsTernaries, NonCapturingCatchVariables, ArbitrayNewExpressions; - use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, RewriteThrowableExpressions; + use + ArbitrayNewExpressions, + ArrayUnpackUsingMerge, + CallablesAsClosures, + MatchAsTernaries, + NonCapturingCatchVariables, + NullsafeAsTernaries, + OmitArgumentNames, + ReadonlyProperties, + RewriteBlockLambdaExpressions, + RewriteClassOnObjects, + RewriteEnums, + RewriteExplicitOctals, + RewriteThrowableExpressions + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index f20f0d53..779c224c 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; /** Sets up type => literal mappings */ public function __construct() { From d2eb37169573f1996a6d835a05f29cc915152335 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 28 Dec 2021 17:20:59 +0100 Subject: [PATCH 542/926] Fix isConstant() --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 661fbff6..20cea0e1 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -48,8 +48,8 @@ protected function isConstant($result, $node) { if ($node instanceof Literal) { return true; } else if ($node instanceof ArrayLiteral) { - foreach ($node->values as $node) { - if (!$this->isConstant($result, $node)) return false; + foreach ($node->values as $element) { + if (!$this->isConstant($result, $element[1])) return false; } return true; } else if ($node instanceof ScopeExpression) { From cea9273384fb1339da3671c76d1e2be4e47d71ca Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 28 Dec 2021 17:22:29 +0100 Subject: [PATCH 543/926] No longer rewrite callable syntax in PHP 8.1 --- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 35cb035d..1317fc28 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions, CallablesAsClosures; + use RewriteBlockLambdaExpressions; /** Sets up type => literal mappings */ public function __construct() { From 04da5e68ba0ea2c0bcd4f6668b043035de368784 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 28 Dec 2021 17:23:48 +0100 Subject: [PATCH 544/926] Release 7.2.1 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 1c3d9c47..ec8a785c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 7.2.1 / 2021-12-28 + +* Fixed PHP 8.1 not emitting native callable syntax - @thekid +* Fixed `isConstant()` for constant arrays - @thekid + ## 7.2.0 / 2021-12-20 * Optimized generated code for arrays including unpack expressions for From ff4189025412daa944e387d05f67cc150a8949a6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 28 Dec 2021 17:31:30 +0100 Subject: [PATCH 545/926] QA: Add missing comma [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54aae841..f97c2bd1 100755 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ The -q option suppresses all diagnostic output except for errors. Features supported ------------------ -XP Compiler supports features such as annotations, arrow functions, enums property type-hints, the null-safe instance operator as well as all PHP 7 and PHP 8 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). +XP Compiler supports features such as annotations, arrow functions, enums, property type-hints, the null-safe instance operator as well as all PHP 7 and PHP 8 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). Additional syntax can be added by installing compiler plugins from [here](https://github.com/xp-lang): From 4bf6478346d05e10cc30fc433e5430f200616f10 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 28 Dec 2021 17:44:11 +0100 Subject: [PATCH 546/926] Do not emit XP Meta information by default See #116 --- composer.json | 2 +- .../lang/ast/CompilingClassloader.class.php | 9 +- src/main/php/lang/ast/Emitter.class.php | 25 +++- src/main/php/lang/ast/Result.class.php | 14 ++ src/main/php/lang/ast/emit/PHP.class.php | 135 +++++++----------- src/main/php/lang/ast/emit/XPMeta.class.php | 104 ++++++++++++++ .../php/xp/compiler/CompileRunner.class.php | 6 +- .../lang/ast/unittest/ResultTest.class.php | 32 ++++- .../ast/unittest/emit/EmittingTest.class.php | 3 +- .../emit/RewriteClassOnObjectsTest.class.php | 2 +- .../loader/CompilingClassLoaderTest.class.php | 4 +- 11 files changed, 239 insertions(+), 97 deletions(-) create mode 100755 src/main/php/lang/ast/emit/XPMeta.class.php diff --git a/composer.json b/composer.json index acd8632e..48039057 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": "^7.7", + "xp-framework/ast": "dev-master as 8.0.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 1027374c..a3ec90a1 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,6 +1,6 @@ version= $emit->getSimpleName(); + $this->version= str_replace('⋈', '+', $emit->getSimpleName()); + Compiled::$emit[$this->version]= $emit->newInstance(); stream_wrapper_register($this->version, Compiled::class); } @@ -246,7 +247,7 @@ public function instanceId() { * @return lang.IClassLoader */ public static function instanceFor($version) { - $emit= Emitter::forRuntime($version); + $emit= Emitter::forRuntime($version, [XPMeta::class]); $id= $emit->getName(); if (!isset(self::$instance[$id])) { @@ -261,7 +262,7 @@ public static function instanceFor($version) { * @return string */ public function toString() { - return 'CompilingCL<'.$this->version.'>'; + return 'CompilingCL<'.nameof(Compiled::$emit[$this->version]).'>'; } /** diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 039aa282..53c12927 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -2,7 +2,7 @@ use lang\ast\{Node, Error, Errors}; use lang\reflect\Package; -use lang\{IllegalArgumentException, IllegalStateException}; +use lang\{IllegalArgumentException, IllegalStateException, ClassLoader, XPClass}; abstract class Emitter { private $transformations= []; @@ -11,17 +11,34 @@ abstract class Emitter { * Selects the correct emitter for a given runtime * * @param string $runtime E.g. "php:".PHP_VERSION + * @param string[]|lang.XPClass[] $emitters Optional * @return lang.XPClass * @throws lang.IllegalArgumentException */ - public static function forRuntime($runtime) { + public static function forRuntime($runtime, $emitters= []) { sscanf($runtime, '%[^.:]%*[.:]%d.%d', $engine, $major, $minor); $p= Package::forName('lang.ast.emit'); $engine= strtoupper($engine); do { $impl= $engine.$major.$minor; - if ($p->providesClass($impl)) return $p->loadClass($impl); + if ($p->providesClass($impl)) { + if (empty($emitters)) return $p->loadClass($impl); + + // Extend loaded class, including all given emitters + $extended= ['kind' => 'class', 'extends' => [$p->loadClass($impl)], 'implements' => [], 'use' => []]; + foreach ($emitters as $class) { + if ($class instanceof XPClass) { + $impl.= '⋈'.$class->getSimpleName(); + $extended['use'][]= $class; + } else { + $d= strrpos(strtr($class, '\\', '.'), '.'); + $impl.= '⋈'.(false === $d ? $class : substr($class, $d + 1)); + $extended['use'][]= XPClass::forName($class); + } + } + return ClassLoader::defineType($p->getName().'.'.$impl, $extended, '{}'); + } } while ($minor-- > 0); throw new IllegalArgumentException('XP Compiler does not support '.$runtime.' yet'); @@ -111,6 +128,8 @@ public function emitAll($result, $nodes) { * @return void */ public function emitOne($result, $node) { + + // Inlined Result::at() if ($node->line > $result->line) { $result->out->write(str_repeat("\n", $node->line - $result->line)); $result->line= $node->line; diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 3db2ede2..958145b0 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -32,6 +32,20 @@ public function temp() { return '$'.$this->codegen->symbol(); } + /** + * Forwards output line to given line number + * + * @param int $line + * @return self + */ + public function at($line) { + if ($line > $this->line) { + $this->out->write(str_repeat("\n", $line - $this->line)); + $this->line= $line; + } + return $this; + } + /** * Looks up a given type * diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 20cea0e1..8333f60a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -250,6 +250,7 @@ protected function emitArray($result, $array) { } protected function emitParameter($result, $parameter) { + $parameter->annotations && $this->emitOne($result, $parameter->annotations); if ($parameter->type && $t= $this->literal($parameter->type)) { $result->out->write($t.' '); } @@ -338,7 +339,9 @@ protected function emitEnum($result, $enum) { array_unshift($result->meta, []); $result->locals= [[], []]; - $result->out->write('enum '.$this->declaration($enum->name)); + $enum->comment && $this->emitOne($result, $enum->comment); + $enum->annotations && $this->emitOne($result, $enum->annotations); + $result->at($enum->declared)->out->write('enum '.$this->declaration($enum->name)); $enum->base && $result->out->write(':'.$enum->base); $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); $result->out->write('{'); @@ -360,7 +363,9 @@ protected function emitClass($result, $class) { array_unshift($result->meta, []); $result->locals= [[], [], []]; - $result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); + $class->comment && $this->emitOne($result, $class->comment); + $class->annotations && $this->emitOne($result, $class->annotations); + $result->at($class->declared)->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); $class->parent && $result->out->write(' extends '.$class->parent); $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); $result->out->write('{'); @@ -408,94 +413,53 @@ protected function emitClass($result, $class) { array_shift($result->type); } - /** Stores lowercased, unnamespaced name in annotations for BC reasons! */ - protected function annotations($result, $annotations) { - $lookup= []; - foreach ($annotations as $name => $arguments) { - $p= strrpos($name, '\\'); - $key= lcfirst(false === $p ? $name : substr($name, $p + 1)); - $result->out->write("'".$key."' => "); - $name === $key || $lookup[$key]= $name; + protected function emitMeta($result, $name, $annotations, $comment) { + // NOOP + } - if (empty($arguments)) { - $result->out->write('null,'); - } else if (1 === sizeof($arguments) && isset($arguments[0])) { - $this->emitOne($result, $arguments[0]); - $result->out->write(','); - } else { - $result->out->write('['); - foreach ($arguments as $name => $argument) { - is_string($name) && $result->out->write("'".$name."' => "); - $this->emitOne($result, $argument); - $result->out->write(','); - } - $result->out->write('],'); - } - } - return $lookup; + protected function emitComment($result, $comment) { + $result->out->write($comment->declaration); } - /** Emits annotations in XP format - and mappings for their names */ - protected function attributes($result, $annotations, $target) { - $result->out->write('DETAIL_ANNOTATIONS => ['); - $lookup= $this->annotations($result, $annotations); - $result->out->write('], DETAIL_TARGET_ANNO => ['); - foreach ($target as $name => $annotations) { - $result->out->write("'$".$name."' => ["); - foreach ($this->annotations($result, $annotations) as $key => $value) { - $lookup[$key]= $value; + protected function emitAnnotation($result, $annotation) { + $result->out->write('\\'.$annotation->name); + if ($annotation->arguments) { + + // Check whether arguments are constant + foreach ($annotation->arguments as $argument) { + if ($this->isConstant($result, $argument)) continue; + + // Found first non-constant argument, enclose in `eval` + // FIXME Emit more than one argument + $result->out->write('(eval: \''); + foreach ($annotation->arguments as $name => $argument) { + $this->emitOne($result, $argument); + } + $result->out->write('\')'); + return; } - $result->out->write('],'); - } - foreach ($lookup as $key => $value) { - $result->out->write("'".$key."' => '".$value."',"); - } - $result->out->write(']'); - } - /** Removes leading, intermediate and trailing stars from apidoc comments */ - private function comment($comment) { - if (null === $comment || '' === $comment) { - return 'null'; - } else if ('/' === $comment[0]) { - return "'".str_replace("'", "\\'", trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 3, -2))))."'"; - } else { - return "'".str_replace("'", "\\'", $comment)."'"; + $result->out->write('('); + $this->emitArguments($result, $annotation->arguments); + $result->out->write(')'); } } - /** Emit meta information so that the reflection API won't have to parse it */ - protected function emitMeta($result, $name, $annotations, $comment) { - if (null === $name) { - $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); - } else { - $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); - } - $result->out->write('"class" => ['); - $this->attributes($result, $annotations, []); - $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); - - foreach (array_shift($result->meta) as $type => $lookup) { - $result->out->write($type.' => ['); - foreach ($lookup as $key => $meta) { - $result->out->write("'".$key."' => ["); - $this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]); - $result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); - $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); - $result->out->write(', DETAIL_ARGUMENTS => ['.($meta[DETAIL_ARGUMENTS] - ? "'".implode("', '", $meta[DETAIL_ARGUMENTS])."']]," - : ']],' - )); - } - $result->out->write('],'); + protected function emitAnnotations($result, $annotations) { + $result->out->write('#['); + foreach ($annotations->named as $annotation) { + $this->emitOne($result, $annotation); + $result->out->write(','); } - $result->out->write('];'); + $result->out->write(']'); } protected function emitInterface($result, $interface) { array_unshift($result->meta, []); - $result->out->write('interface '.$this->declaration($interface->name)); + $interface->comment && $this->emitOne($result, $interface->comment); + $interface->annotations && $this->emitOne($result, $interface->annotations); + $result->at($interface->declared)->out->write('interface '.$this->declaration($interface->name)); $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); $result->out->write('{'); foreach ($interface->body as $member) { @@ -509,7 +473,9 @@ protected function emitInterface($result, $interface) { protected function emitTrait($result, $trait) { array_unshift($result->meta, []); - $result->out->write('trait '.$this->declaration($trait->name)); + $trait->comment && $this->emitOne($result, $trait->comment); + $trait->annotations && $this->emitOne($result, $trait->annotations); + $result->at($trait->declared)->out->write('trait '.$this->declaration($trait->name)); $result->out->write('{'); foreach ($trait->body as $member) { $this->emitOne($result, $member); @@ -533,7 +499,9 @@ protected function emitUse($result, $use) { } protected function emitConst($result, $const) { - $result->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); + $const->comment && $this->emitOne($result, $const->comment); + $const->annotations && $this->emitOne($result, $const->annotations); + $result->at($const->declared)->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); $this->emitOne($result, $const->expression); $result->out->write(';'); } @@ -547,7 +515,9 @@ protected function emitProperty($result, $property) { DETAIL_ARGUMENTS => [] ]; - $result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); + $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('='); @@ -582,7 +552,9 @@ protected function emitMethod($result, $method) { DETAIL_ARGUMENTS => [] ]; - $result->out->write(implode(' ', $method->modifiers).' function '.$method->name); + $method->comment && $this->emitOne($result, $method->comment); + $method->annotations && $this->emitOne($result, $method->annotations); + $result->at($method->declared)->out->write(implode(' ', $method->modifiers).' function '.$method->name); $this->emitSignature($result, $method->signature); $promoted= []; @@ -593,10 +565,11 @@ protected function emitMethod($result, $method) { // Create properties from promoted parameters. Do not include default value, this is handled // in emitParameter() already; otherwise we would be emitting it twice. if (isset($param->promote)) { - $promoted[]= new Property(explode(' ', $param->promote), $param->name, $param->type, null, [], null, $param->line); + $promoted[]= new Property(explode(' ', $param->promote), $param->name, $param->type, null, null, null, $param->line); $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); } + // Create a parameter annotation named `default` for non-constant parameter defaults if (isset($param->default) && !$this->isConstant($result, $param->default)) { $meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default]; } diff --git a/src/main/php/lang/ast/emit/XPMeta.class.php b/src/main/php/lang/ast/emit/XPMeta.class.php new file mode 100755 index 00000000..e8b8c0ed --- /dev/null +++ b/src/main/php/lang/ast/emit/XPMeta.class.php @@ -0,0 +1,104 @@ + $arguments) { + $p= strrpos($name, '\\'); + $key= lcfirst(false === $p ? $name : substr($name, $p + 1)); + $result->out->write("'".$key."' => "); + $name === $key || $lookup[$key]= $name; + + if (empty($arguments)) { + $result->out->write('null,'); + } else if (1 === sizeof($arguments) && isset($arguments[0])) { + $this->emitOne($result, $arguments[0]); + $result->out->write(','); + } else { + $result->out->write('['); + foreach ($arguments as $name => $argument) { + is_string($name) && $result->out->write("'".$name."' => "); + $this->emitOne($result, $argument); + $result->out->write(','); + } + $result->out->write('],'); + } + } + return $lookup; + } + + /** Emits annotations in XP format - and mappings for their names */ + private function attributes($result, $annotations, $target) { + $result->out->write('DETAIL_ANNOTATIONS => ['); + $lookup= $this->annotations($result, $annotations); + $result->out->write('], DETAIL_TARGET_ANNO => ['); + foreach ($target as $name => $annotations) { + $result->out->write("'$".$name."' => ["); + foreach ($this->annotations($result, $annotations) as $key => $value) { + $lookup[$key]= $value; + } + $result->out->write('],'); + } + foreach ($lookup as $key => $value) { + $result->out->write("'".$key."' => '".$value."',"); + } + $result->out->write(']'); + } + + /** Emit comment inside meta information */ + private function comment($comment) { + return null === $comment ? 'null' : var_export($comment->content(), true); + } + + /** Emit xp::$meta */ + protected function emitMeta($result, $name, $annotations, $comment) { + if (null === $name) { + $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); + } else { + $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + } + $result->out->write('"class" => ['); + $this->attributes($result, $annotations, []); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); + + foreach (array_shift($result->meta) as $type => $lookup) { + $result->out->write($type.' => ['); + foreach ($lookup as $key => $meta) { + $result->out->write("'".$key."' => ["); + $this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]); + $result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); + $result->out->write(', DETAIL_ARGUMENTS => ['.($meta[DETAIL_ARGUMENTS] + ? "'".implode("', '", $meta[DETAIL_ARGUMENTS])."']]," + : ']],' + )); + } + $result->out->write('],'); + } + $result->out->write('];'); + } + + protected function emitComment($result, $comment) { + // Omit from generated code + } + + protected function emitAnnotation($result, $annotation) { + // Omit from generated code + } + + protected function emitAnnotations($result, $annotations) { + // Omit from generated code + } +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index febc74ac..6f5bfb5c 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -51,6 +51,7 @@ public static function main(array $args) { $target= 'php:'.PHP_VERSION; $in= $out= '-'; $quiet= false; + $emitters= []; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { $target= $args[++$i]; @@ -64,6 +65,9 @@ public static function main(array $args) { $out= null; $in= array_slice($args, $i + 1); break; + } else if ('-e' === $args[$i]) { + $emitter= $args[++$i]; + $emitters[]= $emitter; } else { $in= $args[$i]; $out= $args[$i + 1] ?? '-'; @@ -72,7 +76,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime($target)->newInstance(); + $emit= Emitter::forRuntime($target, $emitters)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 8bd4fa5e..136a5bec 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -45,7 +45,7 @@ public function write() { #[Test] public function lookup_self() { $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], [], null, 1); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); } @@ -53,7 +53,7 @@ public function lookup_self() { #[Test] public function lookup_parent() { $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], [], null, 1); + $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); Assert::equals(new Reflection(Value::class), $r->lookup('parent')); } @@ -61,7 +61,7 @@ public function lookup_parent() { #[Test] public function lookup_named() { $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], [], null, 1); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); } @@ -78,4 +78,30 @@ public function lookup_non_existant() { $r= new Result(new StringWriter(new MemoryOutputStream())); $r->lookup('\\NotFound'); } + + #[Test] + public function line_number_initially_1() { + $r= new Result(new StringWriter(new MemoryOutputStream())); + Assert::equals(1, $r->line); + } + + #[Test, Values([[1, 'at($line)->out->write('test'); + + Assert::equals($expected, $out->bytes()); + Assert::equals($line, $r->line); + } + + #[Test] + public function at_cannot_go_backwards() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out)); + $r->at(0)->out->write('test'); + + Assert::equals('bytes()); + Assert::equals(1, $r->line); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 6997ebdc..dd500ce1 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -2,6 +2,7 @@ use io\streams\{MemoryOutputStream, StringWriter}; use lang\DynamicClassLoader; +use lang\ast\emit\XPMeta; use lang\ast\{CompilingClassLoader, Emitter, Language, Result, Tokens}; use unittest\{After, Assert, TestCase}; use util\cmd\Console; @@ -20,7 +21,7 @@ public function __construct($output= null) { $this->output= $output ? array_flip(explode(',', $output)) : []; $this->cl= DynamicClassLoader::instanceFor(self::class); $this->language= Language::named('PHP'); - $this->emitter= Emitter::forRuntime($this->runtime())->newInstance(); + $this->emitter= Emitter::forRuntime($this->runtime(), [XPMeta::class])->newInstance(); foreach ($this->language->extensions() as $extension) { $extension->setup($this->language, $this->emitter); } diff --git a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php index c0e90e6e..7da479c8 100755 --- a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php @@ -24,7 +24,7 @@ public function rewrites_type_variable() { public function does_not_rewrite_type_literal() { Assert::equals('self::class', $this->emit( new ScopeExpression('self', new Literal('class')), - [new ClassDeclaration([], '\\T', null, [], [], [], null, 1)] + [new ClassDeclaration([], '\\T', null, [], [], null, null, 1)] )); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 6ae71b2f..deae8275 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -54,12 +54,12 @@ public function supports_php($version) { #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); } #[Test] public function hashcode() { - Assert::equals('CPHP70', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); + Assert::equals('CPHP70+XPMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); } #[Test] From d43e77916cfa60220ad4495b67994754e66aeac4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 4 Jan 2022 21:28:53 +0100 Subject: [PATCH 547/926] Add shortcut format "php:xp-meta" referring to lang.ast.emit.php.XpMeta --- .../lang/ast/CompilingClassloader.class.php | 5 +++-- .../XpMeta.class.php} | 4 ++-- .../php/xp/compiler/CompileRunner.class.php | 22 ++++++++++++++++--- .../ast/unittest/emit/EmittingTest.class.php | 4 ++-- .../loader/CompilingClassLoaderTest.class.php | 4 ++-- 5 files changed, 28 insertions(+), 11 deletions(-) rename src/main/php/lang/ast/emit/{XPMeta.class.php => php/XpMeta.class.php} (98%) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index a3ec90a1..536e0b0b 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,6 +1,7 @@ getName(); if (!isset(self::$instance[$id])) { diff --git a/src/main/php/lang/ast/emit/XPMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php similarity index 98% rename from src/main/php/lang/ast/emit/XPMeta.class.php rename to src/main/php/lang/ast/emit/php/XpMeta.class.php index e8b8c0ed..c304c8bb 100755 --- a/src/main/php/lang/ast/emit/XPMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -1,4 +1,4 @@ -output= $output ? array_flip(explode(',', $output)) : []; $this->cl= DynamicClassLoader::instanceFor(self::class); $this->language= Language::named('PHP'); - $this->emitter= Emitter::forRuntime($this->runtime(), [XPMeta::class])->newInstance(); + $this->emitter= Emitter::forRuntime($this->runtime(), [XpMeta::class])->newInstance(); foreach ($this->language->extensions() as $extension) { $extension->setup($this->language, $this->emitter); } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index deae8275..d3417a47 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -54,12 +54,12 @@ public function supports_php($version) { #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); } #[Test] public function hashcode() { - Assert::equals('CPHP70+XPMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); + Assert::equals('CPHP70+XpMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); } #[Test] From d690e0607f3a02761899ebe3408bbc7a48edefea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:21:03 +0100 Subject: [PATCH 548/926] Document how to attach emitters --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f97c2bd1..90b544ec 100755 --- a/README.md +++ b/README.md @@ -61,6 +61,9 @@ $ xp compile -n src/main/php/ # Target PHP 7.4 (default target is current PHP version) $ xp compile -t php:7.4 HelloWorld.php HelloWorld.class.php + +# Emit XP meta information (includes lang.ast.emit.php.XpMeta): +$ xp compile -t php:7.4 -e php:xp-meta -o dist src/main/php ``` The -o and -n options accept multiple input sources following them. From 8e5ee27da35e7dcd3689ba647056b2dff8f6dad0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:24:05 +0100 Subject: [PATCH 549/926] Escape content inside string passed to `eval` --- src/main/php/lang/ast/emit/PHP.class.php | 6 +++++- .../php/lang/ast/unittest/ResultTest.class.php | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8333f60a..ec615e77 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,6 +1,7 @@ out->write('(eval: \''); - foreach ($annotation->arguments as $name => $argument) { + $out= $result->out->stream(); + $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); + foreach ($annotation->arguments as $argument) { $this->emitOne($result, $argument); } + $result->out->redirect($out); $result->out->write('\')'); return; } diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 136a5bec..f15bf8f9 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -2,7 +2,7 @@ use io\streams\{StringWriter, MemoryOutputStream}; use lang\ast\Result; -use lang\ast\emit\{Declaration, Reflection}; +use lang\ast\emit\{Declaration, Escaping, Reflection}; use lang\ast\nodes\ClassDeclaration; use lang\{Value, ClassNotFoundException}; use unittest\{Assert, Expect, Test}; @@ -42,6 +42,20 @@ public function write() { Assert::equals('bytes()); } + #[Test] + public function write_escaped() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out)); + + $r->out->write("'"); + $r->out->redirect(new Escaping($out, ["'" => "\\'"])); + $r->out->write("echo 'Hello'"); + $r->out->redirect($out); + $r->out->write("'"); + + Assert::equals("bytes()); + } + #[Test] public function lookup_self() { $r= new Result(new StringWriter(new MemoryOutputStream())); From c91f71f5e34ee00ff1cbce8297417c349b93eb0a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:24:52 +0100 Subject: [PATCH 550/926] Emit PHP 8 attributes as comments in PHP 7 --- .../ast/emit/AttributesAsComments.class.php | 39 ++++++ src/main/php/lang/ast/emit/Escaping.class.php | 24 ++++ src/main/php/lang/ast/emit/PHP70.class.php | 1 + src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + src/main/php/lang/ast/emit/PHP74.class.php | 1 + .../unittest/emit/AnnotationSupport.class.php | 129 ++++++++++++++++++ .../unittest/emit/AnnotationsTest.class.php | 110 +-------------- .../unittest/emit/AttributesTest.class.php | 15 ++ .../ast/unittest/emit/EmittingTest.class.php | 9 +- 10 files changed, 224 insertions(+), 106 deletions(-) create mode 100755 src/main/php/lang/ast/emit/AttributesAsComments.class.php create mode 100755 src/main/php/lang/ast/emit/Escaping.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/AttributesTest.class.php diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php new file mode 100755 index 00000000..fda99112 --- /dev/null +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -0,0 +1,39 @@ +out->write('\\'.$annotation->name); + if (empty($annotation->arguments)) return; + + // We can use named arguments here as PHP 8 attributes are parsed + // by the XP reflection API when using PHP 7. However, we may not + // emit trailing commas here! + $result->out->write('('); + $i= 0; + foreach ($annotation->arguments as $name => $argument) { + $i++ > 0 && $result->out->write(','); + is_string($name) && $result->out->write("{$name}:"); + $this->emitOne($result, $argument); + } + $result->out->write(')'); + } + + protected function emitAnnotations($result, $annotations) { + $result->out->write('#['); + foreach ($annotations->named as $annotation) { + $this->emitOne($result, $annotation); + $result->out->write(','); + } + $result->out->write("]\n"); + $result->line++; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Escaping.class.php b/src/main/php/lang/ast/emit/Escaping.class.php new file mode 100755 index 00000000..cf3685fa --- /dev/null +++ b/src/main/php/lang/ast/emit/Escaping.class.php @@ -0,0 +1,24 @@ +target= $target; + $this->replacements= $replacements; + } + + public function write($bytes) { + $this->target->write(strtr($bytes, $this->replacements)); + } + + public function flush() { + $this->target->flush(); + } + + public function close() { + $this->target->close(); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 596b033d..6962bb4b 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -13,6 +13,7 @@ class PHP70 extends PHP { use ArbitrayNewExpressions, ArrayUnpackUsingMerge, + AttributesAsComments, MatchAsTernaries, NullsafeAsTernaries, OmitArgumentNames, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 3fae3c60..4aabf251 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -11,6 +11,7 @@ class PHP71 extends PHP { use ArbitrayNewExpressions, ArrayUnpackUsingMerge, + AttributesAsComments, CallablesAsClosures, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index bd6727dc..2c9aa978 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -11,6 +11,7 @@ class PHP72 extends PHP { use ArbitrayNewExpressions, ArrayUnpackUsingMerge, + AttributesAsComments, CallablesAsClosures, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 1d9c1d81..bf7c1e87 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -11,6 +11,7 @@ class PHP74 extends PHP { use ArbitrayNewExpressions, ArrayUnpackUsingMerge, + AttributesAsComments, CallablesAsClosures, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php new file mode 100755 index 00000000..c29c3736 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -0,0 +1,129 @@ +type('#[Test] class { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[Test] + public function within_namespace() { + $t= $this->type('namespace tests; #[Test] class { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[Test] + public function resolved_against_import() { + $t= $this->type('use unittest\Test; #[Test] class { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[Test] + public function primitive_value() { + $t= $this->type('#[Author("Timm")] class { }'); + Assert::equals(['author' => 'Timm'], $t->getAnnotations()); + } + + #[Test] + public function array_value() { + $t= $this->type('#[Authors(["Timm", "Alex"])] class { }'); + Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); + } + + #[Test] + public function map_value() { + $t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class { }'); + Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + } + + #[Test] + public function named_argument() { + $t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class { }'); + Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + } + + #[Test] + public function closure_value() { + $t= $this->type('#[Verify(function($arg) { return $arg; })] class { }'); + $f= $t->getAnnotation('verify'); + Assert::equals('test', $f('test')); + } + + #[Test] + public function arrow_function_value() { + $t= $this->type('#[Verify(fn($arg) => $arg)] class { }'); + $f= $t->getAnnotation('verify'); + Assert::equals('test', $f('test')); + } + + #[Test] + public function array_of_arrow_function_value() { + $t= $this->type('#[Verify([fn($arg) => $arg])] class { }'); + $f= $t->getAnnotation('verify'); + Assert::equals('test', $f[0]('test')); + } + + #[Test] + public function single_quoted_string_inside_non_constant_expression() { + $t= $this->type('#[Verify(fn($arg) => \'php\\\\\'.$arg)] class { }'); + $f= $t->getAnnotation('verify'); + Assert::equals('php\\test', $f('test')); + } + + #[Test] + public function has_access_to_class() { + $t= $this->type('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }'); + Assert::equals(['expect' => true], $t->getAnnotations()); + } + + #[Test] + public function method() { + $t= $this->type('class { #[Test] public function fixture() { } }'); + Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); + } + + #[Test] + public function field() { + $t= $this->type('class { #[Test] public $fixture; }'); + Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); + } + + #[Test] + public function param() { + $t= $this->type('class { public function fixture(#[Test] $param) { } }'); + Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); + } + + #[Test] + public function params() { + $t= $this->type('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); + $m= $t->getMethod('fixture'); + Assert::equals( + [['inject' => ['name' => 'a']], ['inject' => null]], + [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] + ); + } + + #[Test] + public function multiple_class_annotations() { + $t= $this->type('#[Resource("/"), Authenticated] class { }'); + Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + } + + #[Test] + public function multiple_member_annotations() { + $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); + Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index f42d167b..dfea94ce 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,116 +1,16 @@ type('#[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); - } + /** @return string[] */ + protected function emitters() { return [XpMeta::class]; } - #[Test] - public function within_namespace() { - $t= $this->type('namespace tests; #[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); - } - - #[Test] - public function resolved_against_import() { - $t= $this->type('use unittest\Test; #[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); - } - - #[Test] - public function primitive_value() { - $t= $this->type('#[Author("Timm")] class { }'); - Assert::equals(['author' => 'Timm'], $t->getAnnotations()); - } - - #[Test] - public function array_value() { - $t= $this->type('#[Authors(["Timm", "Alex"])] class { }'); - Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); - } - - #[Test] - public function map_value() { - $t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class { }'); - Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); - } - - #[Test] - public function named_argument() { - $t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class { }'); - Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); - } - - #[Test] - public function closure_value() { - $t= $this->type('#[Verify(function($arg) { return $arg; })] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f('test')); - } - - #[Test] - public function arrow_function_value() { - $t= $this->type('#[Verify(fn($arg) => $arg)] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f('test')); - } - - #[Test] - public function has_access_to_class() { - $t= $this->type('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }'); - Assert::equals(['expect' => true], $t->getAnnotations()); - } - - #[Test] - public function method() { - $t= $this->type('class { #[Test] public function fixture() { } }'); - Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); - } - - #[Test] - public function field() { - $t= $this->type('class { #[Test] public $fixture; }'); - Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); - } - - #[Test] - public function param() { - $t= $this->type('class { public function fixture(#[Test] $param) { } }'); - Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); - } - - #[Test] - public function params() { - $t= $this->type('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); - $m= $t->getMethod('fixture'); - Assert::equals( - [['inject' => ['name' => 'a']], ['inject' => null]], - [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] - ); - } - - #[Test] - public function multiple_class_annotations() { - $t= $this->type('#[Resource("/"), Authenticated] class { }'); - Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); - } - - #[Test] - public function multiple_member_annotations() { - $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); - Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php b/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php new file mode 100755 index 00000000..c395d437 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php @@ -0,0 +1,15 @@ +output= $output ? array_flip(explode(',', $output)) : []; $this->cl= DynamicClassLoader::instanceFor(self::class); $this->language= Language::named('PHP'); - $this->emitter= Emitter::forRuntime($this->runtime(), [XpMeta::class])->newInstance(); + $this->emitter= Emitter::forRuntime($this->runtime(), $this->emitters())->newInstance(); foreach ($this->language->extensions() as $extension) { $extension->setup($this->language, $this->emitter); } } + /** + * Returns emitters to use. Defaults to XpMeta + * + * @return string[] + */ + protected function emitters() { return [XpMeta::class]; } + /** * Returns runtime to use. Uses `PHP_VERSION` constant. * From bcde44279e48e260bd00767c2f52cec67fa81785 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:35:04 +0100 Subject: [PATCH 551/926] Add support for multiline annotations for PHP 7 --- .../lang/ast/emit/AttributesAsComments.class.php | 16 ++++++++++++---- .../unittest/emit/AnnotationSupport.class.php | 12 ++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index fda99112..2b3cd209 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -2,9 +2,11 @@ /** * Rewrites PHP 8 attributes as comments for PHP 7, ensuring they are - * always on a line by themselves. This might break line numbers and - * make it harder to debug but guarantees the emitted code is syntactically - * correct! + * always on a line by themselves *and* never span more than one line. + * + * This will break line numbers if annotations are on the same line as + * the element they belong to and make it harder to debug but guarantees + * the emitted code is syntactically correct! * * @see https://wiki.php.net/rfc/shorter_attribute_syntax_change */ @@ -28,12 +30,18 @@ protected function emitAnnotation($result, $annotation) { } protected function emitAnnotations($result, $annotations) { + $line= $annotations->line; $result->out->write('#['); + + $out= $result->out->stream(); + $result->out->redirect(new Escaping($out, ["\n" => " "])); foreach ($annotations->named as $annotation) { $this->emitOne($result, $annotation); $result->out->write(','); } + $result->out->redirect($out); + $result->out->write("]\n"); - $result->line++; + $result->line= $line + 1; } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index c29c3736..21fe7b8f 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -126,4 +126,16 @@ public function multiple_member_annotations() { $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); } + + #[Test] + public function multiline_annotations() { + $t= $this->type(' + #[Authors([ + "Timm", + "Mr. Midori", + ])] + class { }' + ); + Assert::equals(['authors' => ['Timm', 'Mr. Midori']], $t->getAnnotations()); + } } \ No newline at end of file From f34c1f751a3d8d607c348a5d17a8ac6df01b6236 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:37:14 +0100 Subject: [PATCH 552/926] QA: Use if-guard and return early instead of long block --- src/main/php/lang/ast/emit/PHP.class.php | 37 ++++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index ec615e77..b2ced035 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -424,29 +424,28 @@ protected function emitComment($result, $comment) { protected function emitAnnotation($result, $annotation) { $result->out->write('\\'.$annotation->name); - if ($annotation->arguments) { + if (empty($annotation->arguments)) return; - // Check whether arguments are constant + // Check whether arguments are constant + foreach ($annotation->arguments as $argument) { + if ($this->isConstant($result, $argument)) continue; + + // Found first non-constant argument, enclose in `eval` + // FIXME Emit more than one argument + $result->out->write('(eval: \''); + $out= $result->out->stream(); + $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); foreach ($annotation->arguments as $argument) { - if ($this->isConstant($result, $argument)) continue; - - // Found first non-constant argument, enclose in `eval` - // FIXME Emit more than one argument - $result->out->write('(eval: \''); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); - foreach ($annotation->arguments as $argument) { - $this->emitOne($result, $argument); - } - $result->out->redirect($out); - $result->out->write('\')'); - return; + $this->emitOne($result, $argument); } - - $result->out->write('('); - $this->emitArguments($result, $annotation->arguments); - $result->out->write(')'); + $result->out->redirect($out); + $result->out->write('\')'); + return; } + + $result->out->write('('); + $this->emitArguments($result, $annotation->arguments); + $result->out->write(')'); } protected function emitAnnotations($result, $annotations) { From 851fe3c5a07d39f82be9679fc569cdd2bc0b1bb7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 00:47:48 +0100 Subject: [PATCH 553/926] Fix multiple values for non-constant annotations --- src/main/php/lang/ast/emit/PHP.class.php | 16 +++++++++++++--- .../unittest/emit/AnnotationSupport.class.php | 7 +++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b2ced035..25447d6a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -431,13 +431,23 @@ protected function emitAnnotation($result, $annotation) { if ($this->isConstant($result, $argument)) continue; // Found first non-constant argument, enclose in `eval` - // FIXME Emit more than one argument $result->out->write('(eval: \''); $out= $result->out->stream(); $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); - foreach ($annotation->arguments as $argument) { - $this->emitOne($result, $argument); + + // If exactly one unnamed argument exists, emit its value directly + if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { + $this->emitOne($result, current($annotation->arguments)); + } else { + $result->out->write('['); + foreach ($annotation->arguments as $key => $argument) { + $result->out->write("'{$key}'=>"); + $this->emitOne($result, $argument); + $result->out->write(','); + } + $result->out->write(']'); } + $result->out->redirect($out); $result->out->write('\')'); return; diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index 21fe7b8f..53eab000 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -74,6 +74,13 @@ public function array_of_arrow_function_value() { Assert::equals('test', $f[0]('test')); } + #[Test] + public function named_arrow_function_value() { + $t= $this->type('#[Verify(func: fn($arg) => $arg)] class { }'); + $f= $t->getAnnotation('verify'); + Assert::equals('test', $f['func']('test')); + } + #[Test] public function single_quoted_string_inside_non_constant_expression() { $t= $this->type('#[Verify(fn($arg) => \'php\\\\\'.$arg)] class { }'); From 62dc338e942a615ec945e76441196b0bc117f915 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 02:01:39 +0100 Subject: [PATCH 554/926] Make non-constant parameter defaults work w/o XP meta --- src/main/php/lang/ast/emit/PHP.class.php | 31 ++++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 25447d6a..fa7ed032 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,8 +1,7 @@ comment && $this->emitOne($result, $method->comment); $method->annotations && $this->emitOne($result, $method->annotations); $result->at($method->declared)->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $this->emitSignature($result, $method->signature); $promoted= []; foreach ($method->signature->parameters as $param) { - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; - $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + + // Create a parameter annotation named `default` for non-constant parameter defaults + if (isset($param->default) && !$this->isConstant($result, $param->default)) { + $param->annotate(new Annotation('default', [$param->default])); + } // Create properties from promoted parameters. Do not include default value, this is handled // in emitParameter() already; otherwise we would be emitting it twice. if (isset($param->promote)) { - $promoted[]= new Property(explode(' ', $param->promote), $param->name, $param->type, null, null, null, $param->line); - $result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name); + $promoted[]= [ + $param->reference, + new Property(explode(' ', $param->promote), $param->name, $param->type, null, null, null, $param->line) + ]; } - // Create a parameter annotation named `default` for non-constant parameter defaults - if (isset($param->default) && !$this->isConstant($result, $param->default)) { - $meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default]; - } + $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; + $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } + $this->emitSignature($result, $method->signature); if (null === $method->body) { $result->out->write(';'); } else { $result->out->write(' {'); $this->emitInitializations($result, $result->locals[1]); + foreach ($promoted as $promote) { + $result->out->write('$this->'.$promote[1]->name.($promote[0] ? '=&$' : '=$').$promote[1]->name.';'); + } $this->emitAll($result, $method->body); $result->out->write('}'); } - foreach ($promoted as $property) { - $this->emitOne($result, $property); + foreach ($promoted as $promote) { + $this->emitOne($result, $promote[1]); } // Copy any virtual properties inside locals[2] to class scope From ebc4d292c7924e09a0a3545f97c832d0af969da8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 02:12:13 +0100 Subject: [PATCH 555/926] Simplify parameter promotion --- src/main/php/lang/ast/emit/PHP.class.php | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index fa7ed032..74f253a9 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,7 @@ signature->parameters as $param) { + if (isset($param->promote)) $promoted[]= $param; // Create a parameter annotation named `default` for non-constant parameter defaults if (isset($param->default) && !$this->isConstant($result, $param->default)) { $param->annotate(new Annotation('default', [$param->default])); } - // Create properties from promoted parameters. Do not include default value, this is handled - // in emitParameter() already; otherwise we would be emitting it twice. - if (isset($param->promote)) { - $promoted[]= [ - $param->reference, - new Property(explode(' ', $param->promote), $param->name, $param->type, null, null, null, $param->line) - ]; - } - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; } @@ -595,15 +587,15 @@ protected function emitMethod($result, $method) { } else { $result->out->write(' {'); $this->emitInitializations($result, $result->locals[1]); - foreach ($promoted as $promote) { - $result->out->write('$this->'.$promote[1]->name.($promote[0] ? '=&$' : '=$').$promote[1]->name.';'); + foreach ($promoted as $param) { + $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } $this->emitAll($result, $method->body); $result->out->write('}'); } - foreach ($promoted as $promote) { - $this->emitOne($result, $promote[1]); + foreach ($promoted as $param) { + $result->out->write($param->promote.' '.$this->propertyType($param->type).' $'.$param->name.';'); } // Copy any virtual properties inside locals[2] to class scope From 7018f16f3607f310797495f5498ee7666d8ac161 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 20:33:58 +0100 Subject: [PATCH 556/926] Fix PHP < 8.1 --- src/main/php/lang/ast/emit/PHP.class.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 74f253a9..4bf12cee 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,18 @@ out->write($param->promote.' '.$this->propertyType($param->type).' $'.$param->name.';'); + $this->emitProperty($result, new Property(explode(' ', $param->promote), $param->name, $param->type)); } // Copy any virtual properties inside locals[2] to class scope From 68d584767530fd6b25da6fda6b590a4ffd62c114 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 20:40:01 +0100 Subject: [PATCH 557/926] Use shorter version string --- src/main/php/lang/ast/CompilingClassloader.class.php | 2 +- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 536e0b0b..2f509f59 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -263,7 +263,7 @@ public static function instanceFor($version) { * @return string */ public function toString() { - return 'CompilingCL<'.nameof(Compiled::$emit[$this->version]).'>'; + return 'CompilingCL<'.$this->version.'>'; } /** diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index d3417a47..6188491c 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -54,7 +54,7 @@ public function supports_php($version) { #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); } #[Test] From 36fa05cddbf7826beaa12cf39e22b3e340f1c225 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 21:07:20 +0100 Subject: [PATCH 558/926] Ignore coverage for static initializer --- src/main/php/lang/ast/emit/Reflection.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index f28568d9..858439b4 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -6,6 +6,7 @@ class Reflection extends Type { private $reflect; private static $UNITENUM; + /** @codeCoverageIgnore */ static function __static() { self::$UNITENUM= interface_exists(\UnitEnum::class, false); // Compatibility with XP < 10.8.0 } From 4f1e147bbbd23b1ba136b067524f590fb622e984 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 21:07:38 +0100 Subject: [PATCH 559/926] Add tests for all lang.ast.emit.Escaping methods --- .../ast/unittest/emit/EscapingTest.class.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/EscapingTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php new file mode 100755 index 00000000..9a9e8e31 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php @@ -0,0 +1,54 @@ + "\\'"]); + $fixture->write("'Hello', he said"); + + Assert::equals("\\'Hello\\', he said", $out->bytes()); + } + + #[Test] + public function calls_underlying_flush() { + $out= new class() extends MemoryOutputStream { + public $flushed= false; + + public function flush() { + $this->flushed= true; + parent::flush(); + } + }; + $fixture= new Escaping($out, []); + $fixture->flush(); + + Assert::true($out->flushed); + } + + #[Test] + public function calls_underlying_close() { + $out= new class() extends MemoryOutputStream { + public $closed= false; + + public function close() { + $this->closed= true; + parent::close(); + } + }; + $fixture= new Escaping($out, []); + $fixture->close(); + + Assert::true($out->closed); + } +} \ No newline at end of file From 6cbe1864ff5554abc4eaa8d96ae2e719e46c8c6a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 21:08:10 +0100 Subject: [PATCH 560/926] Ignore coverage for static initializer --- src/main/php/lang/ast/emit/Type.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 10b455e7..be458366 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -3,6 +3,7 @@ abstract class Type { public static $ENUMS; + /** @codeCoverageIgnore */ static function __static() { self::$ENUMS= PHP_VERSION_ID >= 80100; } From c955a742afd71884a8d800eaed3d388dd289bfad Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 21:22:41 +0100 Subject: [PATCH 561/926] Add tests for intersection types --- src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index e76ac535..b7d8afac 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -1,7 +1,7 @@ Date: Wed, 5 Jan 2022 21:25:36 +0100 Subject: [PATCH 562/926] Add PHP 8.2 --- .../lang/ast/unittest/TypeLiteralsTest.class.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index b7d8afac..b176a07b 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -1,6 +1,6 @@ php81(); + } + #[Test, Values('php70')] public function php70_literals($type, $literal) { Assert::equals($literal, (new PHP70())->literal($type)); @@ -138,4 +147,9 @@ public function php80_literals($type, $literal) { public function php81_literals($type, $literal) { Assert::equals($literal, (new PHP81())->literal($type)); } + + #[Test, Values('php82')] + public function php82_literals($type, $literal) { + Assert::equals($literal, (new PHP82())->literal($type)); + } } \ No newline at end of file From 0e9b588e809098848550c94b23825372d335fbd8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 5 Jan 2022 23:58:01 +0100 Subject: [PATCH 563/926] Add a bit of text describing each major release --- ChangeLog.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index ec8a785c..addc2161 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -23,6 +23,8 @@ XP Compiler ChangeLog ## 7.0.0 / 2021-10-21 +This major release drops compatiblity with older XP versions. + * Made compatible with XP 11 - @thekid * Implemented xp-framework/rfc#341, dropping compatibility with XP 9 (@thekid) @@ -134,6 +136,10 @@ XP Compiler ChangeLog ## 6.0.0 / 2020-11-28 +This major release removes legacy XP and Hack language annotations as +well as curly braces for string and array offsets. It also includes the +first PHP 8.1 features. + * Added `-q` command line option which suppresses all diagnostic output from the compiler except for errors (@thekid) @@ -232,6 +238,9 @@ XP Compiler ChangeLog ## 5.0.0 / 2019-11-30 +This major release drops PHP 5 support. The minimum required PHP version +is now 7.0.0. + * Merged PR #70: Extract compact methods; to use these, require the library https://github.com/xp-lang/php-compact-methods (@thekid) @@ -280,6 +289,10 @@ XP Compiler ChangeLog ## 4.0.0 / 2019-09-09 +This major release adds an extension mechanisms. Classes inside the package +`lang.ast.syntax.php` (regardless of their class path) will be loaded auto- +matically on startup. + * Merged PR #69: Remove support for Hack arrow functions - @thekid * Fixed operator precedence for unary prefix operators - @thekid * Merged PR #66: Syntax plugins. With this facility in place, the compiler @@ -291,6 +304,10 @@ XP Compiler ChangeLog ## 3.0.0 / 2019-08-10 +This release aligns XP Compiler compatible with PHP 7.4 and changes it +to try to continue parsing after an error has occured, possibly yielding +multiple errors. + * Made compatible with PHP 7.4 - refrain using `{}` for string offsets (@thekid) * Merged PR #45 - Multiple errors - @thekid @@ -445,6 +462,10 @@ XP Compiler ChangeLog ## 2.0.0 / 2017-11-06 +This major release extracts the AST API to its own library, and cleans it +up while doing so. The idea is to be able to use this library in other +places in the future. + * Implemented `use function` and `use const` - @thekid * Fixed issue #21: Comments are not escaped - @thekid * Project [AST API](https://github.com/xp-framework/compiler/projects/1): @@ -497,6 +518,11 @@ XP Compiler ChangeLog ## 1.0.0 / 2017-10-25 +This first release brings consistency to annotations, casting and how +and where keywords can be used. XP Compiler is already being used in +production in an internal project at the time of writing, so you might +do so too:) + * Indexed type members by name; implementing feature suggested in #10 (@thekid) * **Heads up:** Implemented syntax for parameter annotations as stated From 0daf7f54d1b3c7ba2ac10b99b89c96ed3079fd55 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 6 Jan 2022 00:07:14 +0100 Subject: [PATCH 564/926] QA: Explain how to run code --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f97c2bd1..7ce4ad0d 100755 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ class HelloWorld { } ``` +To run this code, use `xp HelloWorld` in a terminal window. + Compilation ----------- Compilation can also be performed explicitely by invoking the compiler: From 36c32d83e7660e2f9046b724bfe5fc06c61ec51e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 6 Jan 2022 00:08:18 +0100 Subject: [PATCH 565/926] Make compatible with AST library version 8.0.0 --- ChangeLog.md | 4 ++++ composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index addc2161..7f33daca 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 7.3.0 / ????-??-?? + +* Made compatible with `xp-framework/ast` versiom 8.0.0 - @thekid + ## 7.2.1 / 2021-12-28 * Fixed PHP 8.1 not emitting native callable syntax - @thekid diff --git a/composer.json b/composer.json index acd8632e..3c6e345c 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": "^7.7", + "xp-framework/ast": "^8.0 | ^7.7", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 20cea0e1..85202837 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,7 +1,18 @@ $arguments) { $p= strrpos($name, '\\'); @@ -457,6 +470,8 @@ protected function attributes($result, $annotations, $target) { private function comment($comment) { if (null === $comment || '' === $comment) { return 'null'; + } else if ($comment instanceof Comment) { + return var_export($comment->content(), true); } else if ('/' === $comment[0]) { return "'".str_replace("'", "\\'", trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 3, -2))))."'"; } else { From 0a2ab7151da4518481873b7775e9c257f08796a0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 6 Jan 2022 18:09:04 +0100 Subject: [PATCH 566/926] Add support for static closures --- src/main/php/lang/ast/emit/PHP.class.php | 11 +++++---- .../RewriteBlockLambdaExpressions.class.php | 2 +- .../emit/RewriteLambdaExpressions.class.php | 2 +- .../RewriteThrowableExpressions.class.php | 2 +- .../ast/unittest/emit/LambdasTest.class.php | 24 +++++++++++++++++++ 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 85202837..3ead84a2 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -97,9 +97,10 @@ protected function propertyType($type) { * @param lang.ast.Result $result * @param lang.ast.Node $node * @param ?lang.ast.nodes.Signature $signature + * @param bool $static * @param function(lang.ast.Result, lang.ast.Node): void $emit */ - protected function enclose($result, $node, $signature, $emit) { + protected function enclose($result, $node, $signature, $static, $emit) { $capture= []; foreach ($result->codegen->search($node, 'variable') as $var) { if (isset($result->locals[$var->name])) { @@ -111,7 +112,7 @@ protected function enclose($result, $node, $signature, $emit) { $result->stack[]= $result->locals; $result->locals= []; if ($signature) { - $result->out->write('function'); + $static ? $result->out->write('static function') : $result->out->write('function'); $this->emitSignature($result, $signature); foreach ($signature->parameters as $param) { unset($capture[$param->name]); @@ -159,7 +160,7 @@ protected function emitInitializations($result, $init) { protected function emitAsExpression($result, $expression) { if ($expression instanceof Block) { $result->out->write('('); - $this->enclose($result, $expression, null, function($result, $expression) { + $this->enclose($result, $expression, null, false, function($result, $expression) { $this->emitAll($result, $expression->statements); }); $result->out->write(')()'); @@ -312,7 +313,7 @@ protected function emitClosure($result, $closure) { $result->stack[]= $result->locals; $result->locals= []; - $result->out->write('function'); + isset($closure->static) && $closure->static ? $result->out->write('static function') : $result->out->write('function'); $this->emitSignature($result, $closure->signature); if ($closure->use) { @@ -329,7 +330,7 @@ protected function emitClosure($result, $closure) { } protected function emitLambda($result, $lambda) { - $result->out->write('fn'); + isset($lambda->static) && $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); $result->out->write('=>'); $this->emitOne($result, $lambda->body); diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 750d8c4e..1cb0acda 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -12,7 +12,7 @@ trait RewriteBlockLambdaExpressions { protected function emitLambda($result, $lambda) { if ($lambda->body instanceof Block) { - $this->enclose($result, $lambda->body, $lambda->signature, function($result, $body) { + $this->enclose($result, $lambda->body, $lambda->signature, isset($lambda->static) && $lambda->static, function($result, $body) { $this->emitAll($result, $body->statements); }); } else { diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index 3e905c63..1e83f0a3 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -10,7 +10,7 @@ trait RewriteLambdaExpressions { protected function emitLambda($result, $lambda) { - $this->enclose($result, $lambda, $lambda->signature, function($result, $lambda) { + $this->enclose($result, $lambda, $lambda->signature, isset($lambda->static) && $lambda->static, function($result, $lambda) { if ($lambda->body instanceof Block) { $this->emitAll($result, $lambda->body->statements); } else { diff --git a/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php index 1d13a46f..777a9dc7 100755 --- a/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php @@ -9,7 +9,7 @@ trait RewriteThrowableExpressions { protected function emitThrowExpression($result, $throw) { $result->out->write('('); - $this->enclose($result, $throw->expression, null, function($result, $expression) { + $this->enclose($result, $throw->expression, null, false, function($result, $expression) { $result->out->write('throw '); $this->emitOne($result, $expression); $result->out->write(';'); diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 3cdaa742..92e1999b 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -1,6 +1,8 @@ property_exists(LambdaExpression::class, "static"))')] + public function static_fn_does_not_capture_this() { + $r= $this->run('class { + public function run() { + return static fn() => isset($this); + } + }'); + + Assert::false($r()); + } + + #[Test, Action(eval: 'new VerifyThat(fn() => property_exists(ClosureExpression::class, "static"))')] + public function static_function_does_not_capture_this() { + $r= $this->run('class { + public function run() { + return static function() { return isset($this); }; + } + }'); + + Assert::false($r()); + } + #[Test] public function captures_local() { $r= $this->run('class { From 6d6ba508a7437e526cb8dcda89ca3854b4e67ec8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 6 Jan 2022 18:18:26 +0100 Subject: [PATCH 567/926] Fix bug introduced when resolving merge conflicts --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 63791a04..230a79df 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -463,6 +463,10 @@ protected function emitAnnotation($result, $annotation) { $result->out->write('\')'); return; } + + $result->out->write('('); + $this->emitArguments($result, $annotation->arguments); + $result->out->write(')'); } protected function emitAnnotations($result, $annotations) { From 2242397509ef5b1c34b201a54228d9c2a84c18ea Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Jan 2022 00:31:07 +0100 Subject: [PATCH 568/926] QA: Correct spelling in doc comment --- src/main/php/lang/ast/emit/php/XpMeta.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index c304c8bb..0477a21d 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -4,7 +4,8 @@ * Emit meta information so that the XP reflection API won't have to parse * it. Also omits apidoc comments and annotations from the generated code. * - * Code compiled with this optimization in place require using the XP Core! + * Code compiled with this optimization in place requires using the XP Core + * as a dependency! * * @see https://github.com/xp-framework/rfc/issues/336 */ From 61b8851057914619205520152c620f201af785e8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Jan 2022 00:50:52 +0100 Subject: [PATCH 569/926] Require xp-framework/ast release version See https://github.com/xp-framework/ast/releases/tag/v8.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 48039057..429acfec 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": "dev-master as 8.0.0", + "xp-framework/ast": "^8.0", "php" : ">=7.0.0" }, "require-dev" : { From 7568b79584aecc530f7966619d3bc9f5ed56d86e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Jan 2022 00:57:46 +0100 Subject: [PATCH 570/926] Document support for static closures --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 7f33daca..d37b5ddb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,8 @@ XP Compiler ChangeLog ## 7.3.0 / ????-??-?? -* Made compatible with `xp-framework/ast` versiom 8.0.0 - @thekid +* Merged PR #128: Add support for static closures - @thekid +* Made compatible with `xp-framework/ast` version 8.0.0 - @thekid ## 7.2.1 / 2021-12-28 From 04229213971380ef2617c452b0db454d3c24187c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Jan 2022 21:08:43 +0100 Subject: [PATCH 571/926] Require xp-framework/ast version 8.0 to make static closures work --- ChangeLog.md | 2 +- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- .../php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php | 2 +- src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index d37b5ddb..e2642d14 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,7 @@ XP Compiler ChangeLog ## 7.3.0 / ????-??-?? * Merged PR #128: Add support for static closures - @thekid -* Made compatible with `xp-framework/ast` version 8.0.0 - @thekid +* Upgraded dependency on `xp-framework/ast` to version 8.0.0 - @thekid ## 7.2.1 / 2021-12-28 diff --git a/composer.json b/composer.json index 3c6e345c..429acfec 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": "^8.0 | ^7.7", + "xp-framework/ast": "^8.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 3ead84a2..8338e2fc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -313,7 +313,7 @@ protected function emitClosure($result, $closure) { $result->stack[]= $result->locals; $result->locals= []; - isset($closure->static) && $closure->static ? $result->out->write('static function') : $result->out->write('function'); + $closure->static ? $result->out->write('static function') : $result->out->write('function'); $this->emitSignature($result, $closure->signature); if ($closure->use) { @@ -330,7 +330,7 @@ protected function emitClosure($result, $closure) { } protected function emitLambda($result, $lambda) { - isset($lambda->static) && $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); + $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); $result->out->write('=>'); $this->emitOne($result, $lambda->body); diff --git a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 1cb0acda..596014cd 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -12,7 +12,7 @@ trait RewriteBlockLambdaExpressions { protected function emitLambda($result, $lambda) { if ($lambda->body instanceof Block) { - $this->enclose($result, $lambda->body, $lambda->signature, isset($lambda->static) && $lambda->static, function($result, $body) { + $this->enclose($result, $lambda->body, $lambda->signature, $lambda->static, function($result, $body) { $this->emitAll($result, $body->statements); }); } else { diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php index 1e83f0a3..d1085011 100755 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php @@ -10,7 +10,7 @@ trait RewriteLambdaExpressions { protected function emitLambda($result, $lambda) { - $this->enclose($result, $lambda, $lambda->signature, isset($lambda->static) && $lambda->static, function($result, $lambda) { + $this->enclose($result, $lambda, $lambda->signature, $lambda->static, function($result, $lambda) { if ($lambda->body instanceof Block) { $this->emitAll($result, $lambda->body->statements); } else { From b07a1a206d9754549c89c1130839520113b3f305 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Jan 2022 21:11:56 +0100 Subject: [PATCH 572/926] Release 7.3.0 --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index e2642d14..b6aea921 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 7.3.0 / ????-??-?? +## 7.3.0 / 2022-01-07 * Merged PR #128: Add support for static closures - @thekid * Upgraded dependency on `xp-framework/ast` to version 8.0.0 - @thekid From 22039476c85019293cb24373aef6f582f7dd821f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Jan 2022 15:42:47 +0100 Subject: [PATCH 573/926] QA: Add note on annotations and PHP 7 --- src/main/php/lang/ast/emit/php/XpMeta.class.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index 0477a21d..fe947530 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -3,9 +3,11 @@ /** * Emit meta information so that the XP reflection API won't have to parse * it. Also omits apidoc comments and annotations from the generated code. + * This is the only way we can add full annotation support to PHP 7 without + * impacting the line numbers! * - * Code compiled with this optimization in place requires using the XP Core - * as a dependency! + * Code compiled with this optimization in place requires using XP Core as + * a dependency! * * @see https://github.com/xp-framework/rfc/issues/336 */ From 6eaf8d6c0920d5217bc0b324c2c517e6c5d06308 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Jan 2022 15:44:25 +0100 Subject: [PATCH 574/926] Document XP meta information change, PR #127 --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b6aea921..c85cf747 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.0.0 / ????-??-?? + +* Merged PR #127: Do not emit XP meta information by default. This is + the first step towards generating code that runs without a dependency + on XP core. + (@thekid) + ## 7.3.0 / 2022-01-07 * Merged PR #128: Add support for static closures - @thekid From abd074a1c9c033dcf026ea26c3925cd80a8a2451 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 14 Jan 2022 22:32:31 +0100 Subject: [PATCH 575/926] Fix virtual properties created in class scope being overwritten --- src/main/php/lang/ast/emit/PHP.class.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0778867b..7f5d0a46 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -612,7 +612,11 @@ protected function emitMethod($result, $method) { } // Copy any virtual properties inside locals[2] to class scope - $result->locals= [2 => $result->locals[2]] + array_pop($result->stack); + $virtual= $result->locals[2]; + $result->locals= array_pop($result->stack); + foreach ($virtual as $name => $access) { + $result->locals[2][$name]= $access; + } $result->meta[0][self::METHOD][$method->name]= $meta; } From 1512f90a5ec77b5ce62255e597c171a23f0fbcbe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 14 Jan 2022 23:12:08 +0100 Subject: [PATCH 576/926] Add emitter to create property type checks for PHP < 7.4 --- .../emit/php/VirtualPropertyTypes.class.php | 76 +++++++++++ .../emit/VirtualPropertyTypesTest.class.php | 120 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100755 src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php new file mode 100755 index 00000000..95386455 --- /dev/null +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -0,0 +1,76 @@ + MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY + ]; + + // Because PHP doesn't have __getStatic() and __setStatic(), we cannot simulate + // property type checks for static members. + if (null === $property->type || in_array('static', $property->modifiers)) { + return parent::emitProperty($result, $property); + } + + // Create virtual instance property implementing type coercion and checks + switch ($property->type->name()) { + case 'string': + $assign= 'if (is_scalar($value)) $this->__virtual["%1$s"]= (string)$value;'; + break; + case 'bool': + $assign= 'if (is_scalar($value)) $this->__virtual["%1$s"]= (bool)$value;'; + break; + case 'int': + $assign= 'if (is_numeric($value) || is_bool($value)) $this->__virtual["%1$s"]= (int)$value;'; + break; + case 'float': + $assign= 'if (is_numeric($value) || is_bool($value)) $this->__virtual["%1$s"]= (float)$value;'; + break; + default: + $assign= 'if (is("%2$s", $value)) $this->__virtual["%1$s"]= $value;'; + break; + } + + $result->locals[2][$property->name]= [ + new Code('return $this->__virtual["'.$property->name.'"];'), + new Code(sprintf( + $assign.'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".self::class."::\\$%1$s of type %2$s");', + $property->name, + $property->type->name() + )) + ]; + + // Initialize via constructor + if (isset($property->expression)) { + $result->locals[1]['$this->'.$property->name]= $property->expression; + } + + // Emit XP meta information for the reflection API + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } + $result->meta[0][self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [$modifiers] + ]; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php new file mode 100755 index 00000000..6098ad87 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php @@ -0,0 +1,120 @@ +type('class { + private int $value; + }'); + + Assert::equals(Primitive::$INT, $t->getField('value')->getType()); + } + + #[Test] + public function modifiers_available_via_reflection() { + $t= $this->type('class { + private int $value; + }'); + + Assert::equals(MODIFIER_PRIVATE, $t->getField('value')->getModifiers()); + } + + #[Test] + public function initial_value_available_via_reflection() { + $t= $this->type('class { + private int $value = 6100; + }'); + + Assert::equals(6100, $t->getField('value')->setAccessible(true)->get($t->newInstance())); + } + + #[Test, Values([[null], ['Test'], [[]]]), Expect(class: Error::class, withMessage: '/property .+::\$value of type int/')] + public function type_checked_at_runtime($in) { + $this->run('class { + private int $value; + + public function run($arg) { + $this->value= $arg; + } + }', $in); + } + + #[Test] + public function value_type_test() { + $handle= new Handle(0); + $r= $this->run('use lang\ast\unittest\emit\Handle; class { + private Handle $value; + + public function run($arg) { + $this->value= $arg; + return $this->value; + } + }', $handle); + + Assert::equals($handle, $r); + } + + #[Test, Values(['', 'Test', 1, 1.5, true, false])] + public function string_type_coercion($in) { + $r= $this->run('class { + private string $value; + + public function run($arg) { + $this->value= $arg; + return $this->value; + } + }', $in); + + Assert::equals((string)$in, $r); + } + + #[Test, Values(['', 'Test', 1, 1.5, true, false])] + public function bool_type_coercion($in) { + $r= $this->run('class { + private bool $value; + + public function run($arg) { + $this->value= $arg; + return $this->value; + } + }', $in); + + Assert::equals((bool)$in, $r); + } + + #[Test, Values(['1', '1.5', 1, 1.5, true, false])] + public function int_type_coercion($in) { + $r= $this->run('class { + private int $value; + + public function run($arg) { + $this->value= $arg; + return $this->value; + } + }', $in); + + Assert::equals((int)$in, $r); + } + + #[Test, Values(['1', '1.5', 1, 1.5, true, false])] + public function float_type_coercion($in) { + $r= $this->run('class { + private float $value; + + public function run($arg) { + $this->value= $arg; + return $this->value; + } + }', $in); + + Assert::equals((float)$in, $r); + } +} \ No newline at end of file From f4135acc35683d4e623005acaf2d0795f245e041 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jan 2022 02:46:20 +0100 Subject: [PATCH 577/926] Implement check for private and protected --- .../emit/php/VirtualPropertyTypes.class.php | 21 +++++++- .../emit/VirtualPropertyTypesTest.class.php | 51 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index 95386455..c39577a5 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -46,10 +46,27 @@ protected function emitProperty($result, $property) { break; } + // Add visibility check for private and protected properties + if (in_array('private', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + ' throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");' + ); + } else if (in_array('protected', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. + ' throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' + ); + } else { + $check= ''; + } + $result->locals[2][$property->name]= [ - new Code('return $this->__virtual["'.$property->name.'"];'), + new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), new Code(sprintf( - $assign.'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".self::class."::\\$%1$s of type %2$s");', + $check.$assign.'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".__CLASS__."::\\$%1$s of type %2$s");', $property->name, $property->type->name() )) diff --git a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php index 6098ad87..23086c66 100755 --- a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php @@ -27,6 +27,57 @@ public function modifiers_available_via_reflection() { Assert::equals(MODIFIER_PRIVATE, $t->getField('value')->getModifiers()); } + #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+::\\$value/')] + public function cannot_read_private_field() { + $t= $this->type('class { + private int $value; + }'); + + $t->newInstance()->value; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+::\\$value/')] + public function cannot_write_private_field() { + $t= $this->type('class { + private int $value; + }'); + + $t->newInstance()->value= 6100; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+::\\$value/')] + public function cannot_read_protected_field() { + $t= $this->type('class { + protected int $value; + }'); + + $t->newInstance()->value; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+::\\$value/')] + public function cannot_write_protected_field() { + $t= $this->type('class { + protected int $value; + }'); + + $t->newInstance()->value= 6100; + } + + #[Test] + public function can_access_protected_field_from_subclass() { + $t= $this->type('class { + protected int $value; + }'); + $i= newinstance($t->getName(), [], [ + 'run' => function() { + $this->value= 6100; + return $this->value; + } + ]); + + Assert::equals(6100, $i->run()); + } + #[Test] public function initial_value_available_via_reflection() { $t= $this->type('class { From 677d1f5b04ef000de8cb52506c3098ede0ab157d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jan 2022 02:53:34 +0100 Subject: [PATCH 578/926] QA: Extend api documentation, whitespace --- .../ast/emit/php/VirtualPropertyTypes.class.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index c39577a5..88c31106 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -4,7 +4,11 @@ /** * Creates __get() and __set() overloads which will create type-checked - * instance properties for PHP < 7.4 + * instance properties for PHP < 7.4. Performs type coercion for scalar + * types just as PHP would w/o strict type checks. + * + * Important: Because PHP doesn't have __getStatic() and __setStatic(), + * we cannot simulate property type checks for static members! * * @see https://wiki.php.net/rfc/typed_properties_v2 */ @@ -21,8 +25,7 @@ protected function emitProperty($result, $property) { 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY ]; - // Because PHP doesn't have __getStatic() and __setStatic(), we cannot simulate - // property type checks for static members. + // Exclude properties w/o type and static properties if (null === $property->type || in_array('static', $property->modifiers)) { return parent::emitProperty($result, $property); } @@ -51,13 +54,13 @@ protected function emitProperty($result, $property) { $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__."::\\$%1$s");' ); } else if (in_array('protected', $property->modifiers)) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - ' throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' + 'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' ); } else { $check= ''; @@ -66,7 +69,8 @@ protected function emitProperty($result, $property) { $result->locals[2][$property->name]= [ new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), new Code(sprintf( - $check.$assign.'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".__CLASS__."::\\$%1$s of type %2$s");', + $check.$assign. + 'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".__CLASS__."::\\$%1$s of type %2$s");', $property->name, $property->type->name() )) From 61dc678142e55270141d563e296afd36082e2db0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 04:27:29 +0100 Subject: [PATCH 579/926] Fix private and protected readonly properties being accessible --- ChangeLog.md | 3 + .../ast/emit/ReadonlyProperties.class.php | 54 ++++++------- .../emit/ReadonlyPropertiesTest.class.php | 81 ++++++++++++++++++- 3 files changed, 109 insertions(+), 29 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c85cf747..facc8164 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 8.0.0 / ????-??-?? +* Fixed private and protected readonly properties being accessible from + any scope in PHP < 8.1 + (@thekid) * Merged PR #127: Do not emit XP meta information by default. This is the first step towards generating code that runs without a dependency on XP core. diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 268de61b..9ebfa550 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -1,6 +1,6 @@ [$modifiers] ]; + // Add visibility check for accessing private and protected properties + if (in_array('private', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");' + ); + } else if (in_array('protected', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' + ); + } else { + $check= ''; + } + // Create virtual property implementing the readonly semantics $result->locals[2][$property->name]= [ - new Code('return $this->__virtual["'.$property->name.'"][0] ?? null;'), - new Code(' - if (isset($this->__virtual["'.$property->name.'"])) { - throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}"); - } - $caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; - $scope= $caller["class"] ?? null; - if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope) { - throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope - ? "scope {$scope}" - : "global scope" - )); - } - $this->__virtual["'.$property->name.'"]= [$value]; - '), + new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $property->name)), + new Code(sprintf( + ($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'). + 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. + '$this->__virtual["%1$s"]= [$value];', + $property->name + )), ]; - - if (isset($property->expression)) { - if ($this->isConstant($result, $property->expression)) { - $result->out->write('='); - $this->emitOne($result, $property->expression); - } else if (in_array('static', $property->modifiers)) { - $result->locals[0]['self::$'.$property->name]= $property->expression; - } else { - $result->locals[1]['$this->'.$property->name]= $property->expression; - } - } } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php index 77af350d..2f65771c 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php @@ -1,7 +1,7 @@ type('class { @@ -31,14 +40,67 @@ public function __construct(public readonly string $fixture) { } Assert::equals('Test', $t->newInstance('Test')->fixture); } + #[Test, Values('modifiers')] + public function reading_from_class($modifiers) { + $t= $this->type('class { + public function __construct('.$modifiers.' readonly string $fixture) { } + + public function run() { return $this->fixture; } + }'); + Assert::equals('Test', $t->newInstance('Test')->run()); + } + #[Test] - public function reading() { + public function reading_public_from_outside() { $t= $this->type('class { public function __construct(public readonly string $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); } + #[Test] + public function reading_protected_from_subclass() { + $t= $this->type('class { + public function __construct(protected readonly string $fixture) { } + }'); + $i= newinstance($t->getName(), ['Test'], [ + 'run' => function() { return $this->fixture; } + ]); + Assert::equals('Test', $i->run()); + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+fixture/')] + public function cannot_read_protected() { + $t= $this->type('class { + public function __construct(protected readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+fixture/')] + public function cannot_write_protected() { + $t= $this->type('class { + public function __construct(protected readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+fixture/')] + public function cannot_read_private() { + $t= $this->type('class { + public function __construct(private readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+fixture/')] + public function cannot_write_private() { + $t= $this->type('class { + public function __construct(private readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } + #[Test] public function assigning_inside_constructor() { $t= $this->type('class { @@ -59,6 +121,14 @@ public function can_be_assigned_via_reflection() { Assert::equals('Test', $i->fixture); } + #[Test, Expect(class: Error::class, withMessage: '/Cannot initialize readonly property .+fixture/')] + public function cannot_initialize_from_outside() { + $t= $this->type('class { + public readonly string $fixture; + }'); + $t->newInstance()->fixture= 'Test'; + } + #[Test, Expect(class: Error::class, withMessage: '/Cannot modify readonly property .+fixture/')] public function cannot_be_set_after_initialization() { $t= $this->type('class { @@ -66,4 +136,11 @@ public function __construct(public readonly string $fixture) { } }'); $t->newInstance('Test')->fixture= 'Modified'; } + + #[Test, Ignore('Until proper error handling facilities exist')] + public function cannot_have_an_initial_value() { + $this->type('class { + public readonly string $fixture= "Test"; + }'); + } } \ No newline at end of file From 21b0e2fbf3bce294179847b8304ba714a8259434 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 22:54:17 +0100 Subject: [PATCH 580/926] Do not rewrite readonly properties --- src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index 88c31106..b5632a62 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -25,8 +25,8 @@ protected function emitProperty($result, $property) { 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY ]; - // Exclude properties w/o type and static properties - if (null === $property->type || in_array('static', $property->modifiers)) { + // Exclude properties w/o type, static and readonly properties + if (null === $property->type || in_array('static', $property->modifiers) || in_array('readonly', $property->modifiers)) { return parent::emitProperty($result, $property); } From 68f449875d7eba6d1c84cc3803e14cef14942275 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 23:04:06 +0100 Subject: [PATCH 581/926] Announce emitter to create property type checks --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index facc8164..4274d933 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,9 @@ XP Compiler ChangeLog ## 8.0.0 / ????-??-?? +* Merged PR #129: Add (optional) emitter to create property type checks + for PHP < 7.4 + (@thekid) * Fixed private and protected readonly properties being accessible from any scope in PHP < 8.1 (@thekid) From 0b98869435dbe520a78c1955db4a5c347864cc1a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 23:09:51 +0100 Subject: [PATCH 582/926] Rename `-e` (emitter) to `-a` (augment) --- src/main/php/xp/compiler/CompileRunner.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index bfff77a8..e549f651 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -37,7 +37,7 @@ * ``` * - Emit XP meta information (includes `lang.ast.emit.php.XpMeta`): * ```sh - * $ xp compile -t php:7.4 -e php:xp-meta -o dist src/main/php + * $ xp compile -t php:7.4 -a php:xp-meta -o dist src/main/php * ``` * * The *-o* and *-n* options accept multiple input sources following them. @@ -48,7 +48,7 @@ */ class CompileRunner { - /** Returns an emitter by a given name */ + /** Returns an emitter to be augment by a given name */ private static function emitter(string $name): XPClass { $p= strpos($name, ':'); if (false === $p) return XPClass::forName($name); @@ -68,7 +68,7 @@ public static function main(array $args) { $target= 'php:'.PHP_VERSION; $in= $out= '-'; $quiet= false; - $emitters= []; + $augment= []; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { $target= $args[++$i]; @@ -82,8 +82,8 @@ public static function main(array $args) { $out= null; $in= array_slice($args, $i + 1); break; - } else if ('-e' === $args[$i]) { - $emitters[]= self::emitter($args[++$i]); + } else if ('-a' === $args[$i]) { + $augment[]= self::emitter($args[++$i]); } else { $in= $args[$i]; $out= $args[$i + 1] ?? '-'; @@ -92,7 +92,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime($target, $emitters)->newInstance(); + $emit= Emitter::forRuntime($target, $augment)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } From e040f34d9552e28feabb93c1eafcae7b12b94e87 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 23:10:31 +0100 Subject: [PATCH 583/926] Rename `-e` (emitter) to `-a` (augment) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37fb446a..91c960e9 100755 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ $ xp compile -n src/main/php/ $ xp compile -t php:7.4 HelloWorld.php HelloWorld.class.php # Emit XP meta information (includes lang.ast.emit.php.XpMeta): -$ xp compile -t php:7.4 -e php:xp-meta -o dist src/main/php +$ xp compile -t php:7.4 -a php:xp-meta -o dist src/main/php ``` The -o and -n options accept multiple input sources following them. From 39f484c968e0a538d3e0af87889b57aae3061f2f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jan 2022 23:15:43 +0100 Subject: [PATCH 584/926] Release 8.0.0 --- ChangeLog.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 4274d933..11a8623e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,9 +3,14 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 8.0.0 / ????-??-?? +## 8.0.0 / 2022-01-16 -* Merged PR #129: Add (optional) emitter to create property type checks +This release is the first in a series of releases to make the XP compiler +more universally useful: Compiled code now doesn't include generated XP +meta information by default, and is thus less dependant on XP Core, see +https://github.com/xp-framework/compiler/projects/4. + +* Merged PR #129: Add augmentable emitter to create property type checks for PHP < 7.4 (@thekid) * Fixed private and protected readonly properties being accessible from From 4cf5dccea07635a40a2270c3ed7899cbc9846618 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 29 Jan 2022 11:24:37 +0100 Subject: [PATCH 585/926] Inline nullable checks when casting --- src/main/php/lang/ast/emit/PHP.class.php | 27 ++++++++++++------- .../ast/unittest/emit/CastingTest.class.php | 13 +++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7f5d0a46..c0971156 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -14,7 +14,7 @@ UnpackExpression, Variable }; -use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap}; +use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable}; use lang\ast\{Emitter, Node, Type}; abstract class PHP extends Emitter { @@ -234,17 +234,26 @@ protected function emitVariable($result, $variable) { protected function emitCast($result, $cast) { static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; - $name= $cast->type->name(); - if ('?' === $name[0]) { - $result->out->write('cast('); - $this->emitOne($result, $cast->expression); - $result->out->write(',\''.$name.'\', false)'); - } else if (isset($native[$name])) { - $result->out->write('('.$cast->type->literal().')'); + // Inline nullable checks using ternaries + if ($cast->type instanceof IsNullable) { + $t= $result->temp(); + $result->out->write('null===('.$t.'='); $this->emitOne($result, $cast->expression); + $result->out->write(')?null:'); + + $name= $cast->type->element->name(); + $expression= new Variable(substr($t, 1)); + } else { + $name= $cast->type->name(); + $expression= $cast->expression; + } + + if (isset($native[$name])) { + $result->out->write('('.$name.')'); + $this->emitOne($result, $expression); } else { $result->out->write('cast('); - $this->emitOne($result, $cast->expression); + $this->emitOne($result, $expression); $result->out->write(',\''.$name.'\')'); } } diff --git a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php index 48676097..d7e0a70b 100755 --- a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php @@ -89,6 +89,19 @@ public function run($value) { )); } + #[Test, Values([null, 'test'])] + public function nullable_string_cast_of_expression_returning($value) { + Assert::equals($value, $this->run( + 'class { + public function run($value) { + $values= [$value]; + return (?string)array_pop($values); + } + }', + $value + )); + } + #[Test] public function cast_braced() { Assert::equals(['test'], $this->run( From 55e11b773b98c4b7da2e9754b2644d2c2c72bd75 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 29 Jan 2022 12:02:56 +0100 Subject: [PATCH 586/926] Release 8.1.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 11a8623e..71ba294c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.1.0 / 2022-01-29 + +* Merged PR #131: Inline nullable checks when casting - @thekid + ## 8.0.0 / 2022-01-16 This release is the first in a series of releases to make the XP compiler From 0e26a1f9651c8a258b6212eb29bd92f700cd9410 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 29 Jan 2022 13:54:57 +0100 Subject: [PATCH 587/926] Replace typeof() by PHP native functionality See #132 --- src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index b5632a62..d202e69b 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -70,7 +70,7 @@ protected function emitProperty($result, $property) { new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), new Code(sprintf( $check.$assign. - 'else throw new \\TypeError("Cannot assign ".typeof($value)." to property ".__CLASS__."::\\$%1$s of type %2$s");', + 'else throw new \\TypeError("Cannot assign ".(is_object($value) ? get_class($value) : gettype($value))." to property ".__CLASS__."::\\$%1$s of type %2$s");', $property->name, $property->type->name() )) From 056e79d5b0794792b30febd1a0de84c2d0fb93c3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jan 2022 11:54:28 +0100 Subject: [PATCH 588/926] Support passing emitter-augmenting class names to CompilingClassLoader::instanceFor() --- ChangeLog.md | 6 ++++++ src/main/php/lang/ast/CompilingClassloader.class.php | 11 ++++++----- src/main/php/lang/ast/Emitter.class.php | 5 ++--- .../loader/CompilingClassLoaderTest.class.php | 12 ++++++++++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 71ba294c..aa6f1e31 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.2.0 / 2022-01-30 + +* Support passing emitter-augmenting class names to the instanceFor() + method of `lang.ast.CompilingClassLoader`. + (@thekid) + ## 8.1.0 / 2022-01-29 * Merged PR #131: Inline nullable checks when casting - @thekid diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 2f509f59..b8f04102 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -28,7 +28,7 @@ static function __static() { /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { - $this->version= str_replace('⋈', '+', $emit->getSimpleName()); + $this->version= strtr($emit->getSimpleName(), ['⋈' => '+', '·' => '.']); Compiled::$emit[$this->version]= $emit->newInstance(); stream_wrapper_register($this->version, Compiled::class); @@ -242,13 +242,14 @@ public function instanceId() { } /** - * Fetch instance of classloader by path + * Fetch instance of classloader by version * - * @param string path the identifier - * @return lang.IClassLoader + * @param string $version + * @return lang.IClassLoader */ public static function instanceFor($version) { - $emit= Emitter::forRuntime($version, [XpMeta::class]); + sscanf($version, "%[^+]+%[^\r]", $emitter, $augmented); + $emit= Emitter::forRuntime($emitter, $augmented ? explode('+', $augmented) : [XpMeta::class]); $id= $emit->getName(); if (!isset(self::$instance[$id])) { diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 53c12927..c7cc5b1a 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -29,11 +29,10 @@ public static function forRuntime($runtime, $emitters= []) { $extended= ['kind' => 'class', 'extends' => [$p->loadClass($impl)], 'implements' => [], 'use' => []]; foreach ($emitters as $class) { if ($class instanceof XPClass) { - $impl.= '⋈'.$class->getSimpleName(); + $impl.= '⋈'.strtr($class->getName(), ['.' => '·']); $extended['use'][]= $class; } else { - $d= strrpos(strtr($class, '\\', '.'), '.'); - $impl.= '⋈'.(false === $d ? $class : substr($class, $d + 1)); + $impl.= '⋈'.strtr($class, ['.' => '·', '\\' => '·']); $extended['use'][]= XPClass::forName($class); } } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 6188491c..1ed682f0 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -54,12 +54,12 @@ public function supports_php($version) { #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); } #[Test] public function hashcode() { - Assert::equals('CPHP70+XpMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); + Assert::equals('CPHP70+lang.ast.emit.php.XpMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); } #[Test] @@ -77,6 +77,14 @@ public function compare() { Assert::equals(1, $cl->compareTo(null), 'does not equal null'); } + #[Test] + public function instanced_for_augmented() { + Assert::equals( + 'PHP80+lang.ast.emit.php.XpMeta', + CompilingClassLoader::instanceFor('php:8.0.0+lang.ast.emit.php.XpMeta')->instanceId() + ); + } + #[Test] public function package_contents() { $contents= $this->compile(['Tests' => ' Date: Sun, 30 Jan 2022 12:00:07 +0100 Subject: [PATCH 589/926] Prevent problems when PHP_VERSION includes extra information E.g. "php:7.0.33-57+ubuntu20.04.1+deb.sury.org+1" --- src/main/php/module.xp | 2 +- src/main/php/xp/compiler/AstRunner.class.php | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 2 +- src/main/php/xp/compiler/Usage.class.php | 4 ++-- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 409bfef3..82f404aa 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -7,7 +7,7 @@ module xp-framework/compiler { /** @return void */ public function initialize() { - ClassLoader::registerLoader(CompilingClassloader::instanceFor('php:'.PHP_VERSION)); + ClassLoader::registerLoader(CompilingClassloader::instanceFor('php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION)); if (!interface_exists(\IDisposable::class, false)) { eval('interface IDisposable { public function __dispose(); }'); diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php index 223389ae..5a5bccfa 100755 --- a/src/main/php/xp/compiler/AstRunner.class.php +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -72,7 +72,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime('php:'.PHP_VERSION)->newInstance(); + $emit= Emitter::forRuntime('php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index e549f651..d311e920 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -65,7 +65,7 @@ private static function emitter(string $name): XPClass { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $target= 'php:'.PHP_VERSION; + $target= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; $in= $out= '-'; $quiet= false; $augment= []; diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 7b3d232c..874ac47f 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -6,7 +6,7 @@ /** @codeCoverageIgnore */ class Usage { - const RUNTIME = 'PHP'; + const RUNTIME = 'php'; /** @return int */ public static function main(array $args) { @@ -20,7 +20,7 @@ public function add($t, $active= false) { } }; - $emitter= Emitter::forRuntime(self::RUNTIME.'.'.PHP_VERSION); + $emitter= Emitter::forRuntime(self::RUNTIME.':'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION); foreach (Package::forName('lang.ast.emit')->getClasses() as $class) { if ($class->isSubclassOf(Emitter::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { $impl->add($class, $class->equals($emitter)); diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 1ed682f0..9132a223 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -10,7 +10,7 @@ class CompilingClassLoaderTest { private static $runtime; static function __static() { - self::$runtime= 'php:'.PHP_VERSION; + self::$runtime= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; } /** From 12dad6b328adc643289487abb894f1057c72f0bc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Feb 2022 23:17:57 +0100 Subject: [PATCH 590/926] Move responsibility for creating result to emitter --- src/main/php/lang/ast/Compiled.class.php | 9 ++---- src/main/php/lang/ast/Emitter.class.php | 27 ++++++++++++++++++ src/main/php/lang/ast/Result.class.php | 24 ++++++++++++---- src/main/php/lang/ast/emit/PHP.class.php | 12 +++++++- .../php/xp/compiler/CompileRunner.class.php | 11 +++----- .../lang/ast/unittest/EmitterTest.class.php | 20 ++++++------- .../lang/ast/unittest/ResultTest.class.php | 28 +++++++++++++------ 7 files changed, 90 insertions(+), 41 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 853a3b02..82b7de6a 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -22,14 +22,9 @@ private static function language($name, $emitter) { } private static function parse($lang, $in, $version, $out, $file) { - $language= isset(self::$lang[$lang]) - ? self::$lang[$lang] - : self::$lang[$lang]= self::language($lang, self::$emit[$version]) - ; - + $language= self::$lang[$lang] ?? self::$lang[$lang]= self::language($lang, self::$emit[$version]); try { - self::$emit[$version]->emitAll(new Result($out), $language->parse(new Tokens($in, $file))->stream()); - return $out; + return self::$emit[$version]->write($language->parse(new Tokens($in, $file))->stream(), $out); } finally { $in->close(); } diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index c7cc5b1a..fc89857b 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,5 +1,6 @@ {'emit'.$node->kind}($result, $node); } + + /** + * Creates result + * + * @param io.streams.OutputStream $target + * @return lang.ast.Result + */ + protected abstract function result($target); + + /** + * Emitter entry point, takes nodes and emits them to the given target. + * + * @param iterable $nodes + * @param io.streams.OutputStream $target + * @return io.streams.OutputStream + * @throws lang.ast.Errors + */ + public function write(iterable $nodes, OutputStream $target) { + $result= $this->result($target); + try { + $this->emitAll($result, $nodes); + return $target; + } finally { + $result->close(); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 958145b0..23360b06 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,8 +1,9 @@ out= $out; - $this->out->write($preamble); + $this->out->write($prolog); + $this->epilog= $epilog; $this->codegen= new CodeGen(); } @@ -65,4 +69,14 @@ public function lookup($type) { return new Reflection($type); } + + /** @return void */ + public function close() { + if (null === $this->out) return; + + // Write epilog, then close and ensure this doesn't happen twice + '' === $this->epilog || $this->out->write($this->epilog); + $this->out->close(); + unset($this->out); + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index c0971156..8d5160d7 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -15,7 +15,7 @@ Variable }; use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable}; -use lang\ast\{Emitter, Node, Type}; +use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { const PROPERTY = 0; @@ -23,6 +23,16 @@ abstract class PHP extends Emitter { protected $literals= []; + /** + * Creates result + * + * @param io.streams.OutputStream $target + * @return lang.ast.Result + */ + protected function result($target) { + return new Result($target, 'newInstance(); + $emit= Emitter::forRuntime($emitter, $augment)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } @@ -107,9 +107,7 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= $lang->parse(new Tokens($source, $file)); - $target= $output->target((string)$path); - $emit->emitAll(new Result($target), $parse->stream()); + $emit->write($lang->parse(new Tokens($source, $file))->stream(), $output->target((string)$path)); $t->stop(); $quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); @@ -121,7 +119,6 @@ public static function main(array $args) { $total++; $time+= $t->elapsedTime(); $source->close(); - $target->close(); } } diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 76844908..3ed5b28e 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -74,39 +74,35 @@ public function emit_node_without_kind() { public function transform_modifying_node() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { $var->name= '_'.$var->name; return $var; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_node() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return new Code('$variables["'.$var->name.'"]'); }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_array() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return [new Code('$variables["'.$var->name.'"]')]; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_null() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return null; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index f15bf8f9..f31d72f5 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -21,23 +21,33 @@ public function creates_unique_temporary_variables() { } #[Test] - public function writes_php_open_tag_as_default_preamble() { + public function prolog_and_epilog_default_to_emtpy_strings() { $out= new MemoryOutputStream(); $r= new Result(new StringWriter($out)); - Assert::equals('bytes()); + Assert::equals('', $out->bytes()); } #[Test, Values(['', 'bytes()); + $r= new Result(new StringWriter($out), $prolog); + $r->close(); + Assert::equals($prolog, $out->bytes()); + } + + #[Test] + public function writes_epilog_on_closing() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out), ''); + $r->close(); + + Assert::equals('', $out->bytes()); } #[Test] public function write() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'out->write('echo "Hello";'); Assert::equals('bytes()); } @@ -45,7 +55,7 @@ public function write() { #[Test] public function write_escaped() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'out->write("'"); $r->out->redirect(new Escaping($out, ["'" => "\\'"])); @@ -102,7 +112,7 @@ public function line_number_initially_1() { #[Test, Values([[1, 'at($line)->out->write('test'); Assert::equals($expected, $out->bytes()); @@ -112,7 +122,7 @@ public function write_at_line($line, $expected) { #[Test] public function at_cannot_go_backwards() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'at(0)->out->write('test'); Assert::equals('bytes()); From 29effe32a701a46e283aa0c646a21c44dbc5d878 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Feb 2022 23:21:05 +0100 Subject: [PATCH 591/926] Restore PHP 7.0 compatibility PHP 7.0 does not have the `iterable` type --- src/main/php/lang/ast/Emitter.class.php | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index fc89857b..2943bc76 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -172,7 +172,7 @@ protected abstract function result($target); * @return io.streams.OutputStream * @throws lang.ast.Errors */ - public function write(iterable $nodes, OutputStream $target) { + public function write($nodes, OutputStream $target) { $result= $this->result($target); try { $this->emitAll($result, $nodes); diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 03cc7341..4858ad20 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -65,13 +65,13 @@ private static function emitter(string $name): XPClass { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $emitter= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; + $target= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; $in= $out= '-'; $quiet= false; $augment= []; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { - $emitter= $args[++$i]; + $target= $args[++$i]; } else if ('-q' === $args[$i]) { $quiet= true; } else if ('-o' === $args[$i]) { @@ -92,7 +92,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime($emitter, $augment)->newInstance(); + $emit= Emitter::forRuntime($target, $augment)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } From fd48145e6309918456ac728666ecbf556d868e06 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 15 Feb 2022 22:10:52 +0100 Subject: [PATCH 592/926] Make lang.ast.Result a slimmer base class, extract code generation --- src/main/php/lang/ast/Result.class.php | 42 +-------- .../php/lang/ast/emit/GeneratedCode.class.php | 60 ++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 8 +- .../lang/ast/unittest/EmitterTest.class.php | 5 +- .../lang/ast/unittest/ResultTest.class.php | 94 +++---------------- .../unittest/emit/EmitterTraitTest.class.php | 5 +- .../ast/unittest/emit/EmittingTest.class.php | 10 +- .../unittest/emit/GeneratedCodeTest.class.php | 82 ++++++++++++++++ 8 files changed, 174 insertions(+), 132 deletions(-) create mode 100755 src/main/php/lang/ast/emit/GeneratedCode.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 23360b06..0b3e5175 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,7 +1,7 @@ out= $out; - $this->out->write($prolog); - $this->epilog= $epilog; $this->codegen= new CodeGen(); } - /** - * Creates a temporary variable and returns its name - * - * @return string - */ - public function temp() { - return '$'.$this->codegen->symbol(); - } - /** * Forwards output line to given line number * @@ -50,32 +36,10 @@ public function at($line) { return $this; } - /** - * Looks up a given type - * - * @param string $type - * @return lang.ast.emit.Type - */ - public function lookup($type) { - if ('self' === $type || 'static' === $type) { - return new Declaration($this->type[0], $this); - } else if ('parent' === $type) { - return $this->lookup($this->type[0]->parent); - } - - foreach ($this->type as $enclosing) { - if ($type === $enclosing->name) return new Declaration($enclosing, $this); - } - - return new Reflection($type); - } - /** @return void */ public function close() { if (null === $this->out) return; - // Write epilog, then close and ensure this doesn't happen twice - '' === $this->epilog || $this->out->write($this->epilog); $this->out->close(); unset($this->out); } diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php new file mode 100755 index 00000000..f986b502 --- /dev/null +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -0,0 +1,60 @@ +write($prolog); + $this->epilog= $epilog; + } + + /** + * Creates a temporary variable and returns its name + * + * @return string + */ + public function temp() { + return '$'.$this->codegen->symbol(); + } + + /** + * Looks up a given type + * + * @param string $type + * @return lang.ast.emit.Type + */ + public function lookup($type) { + if ('self' === $type || 'static' === $type) { + return new Declaration($this->type[0], $this); + } else if ('parent' === $type) { + return $this->lookup($this->type[0]->parent); + } + + foreach ($this->type as $enclosing) { + if ($type === $enclosing->name) return new Declaration($enclosing, $this); + } + + return new Reflection($type); + } + + /** @return void */ + public function close() { + if (null === $this->out) return; + + // Write epilog, then close and ensure this doesn't happen twice + '' === $this->epilog || $this->out->write($this->epilog); + $this->out->close(); + unset($this->out); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8d5160d7..0e401e86 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -30,7 +30,7 @@ abstract class PHP extends Emitter { * @return lang.ast.Result */ protected function result($target) { - return new Result($target, 'out->write('(eval: \''); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); + $out= $result->out; + $result->out= new Escaping($out, ["'" => "\\'", '\\' => '\\\\']); // If exactly one unnamed argument exists, emit its value directly if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { @@ -479,7 +479,7 @@ protected function emitAnnotation($result, $annotation) { $result->out->write(']'); } - $result->out->redirect($out); + $result->out= $out; $result->out->write('\')'); return; } diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 3ed5b28e..e4c4476f 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -65,9 +65,10 @@ public function remove_unsets_empty_kind() { #[Test, Expect(IllegalStateException::class)] public function emit_node_without_kind() { - $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), new class() extends Node { + $node= new class() extends Node { public $kind= null; - }); + }; + $this->newEmitter()->write([$node], new MemoryOutputStream()); } #[Test] diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index f31d72f5..3ded288c 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,6 +1,6 @@ temp(), $r->temp(), $r->temp()]); - } - - #[Test] - public function prolog_and_epilog_default_to_emtpy_strings() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); - Assert::equals('', $out->bytes()); - } - - #[Test, Values(['', 'close(); - Assert::equals($prolog, $out->bytes()); - } - - #[Test] - public function writes_epilog_on_closing() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), ''); - $r->close(); - - Assert::equals('', $out->bytes()); + new Result(new MemoryOutputStream()); } #[Test] public function write() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), 'out->write('echo "Hello";'); - Assert::equals('bytes()); + Assert::equals('echo "Hello";', $out->bytes()); } #[Test] public function write_escaped() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), 'out->write("'"); - $r->out->redirect(new Escaping($out, ["'" => "\\'"])); + $r->out= new Escaping($out, ["'" => "\\'"]); $r->out->write("echo 'Hello'"); - $r->out->redirect($out); - $r->out->write("'"); - - Assert::equals("bytes()); - } - - #[Test] - public function lookup_self() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); - - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); - } - - #[Test] - public function lookup_parent() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); - - Assert::equals(new Reflection(Value::class), $r->lookup('parent')); - } - #[Test] - public function lookup_named() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); - - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); - } - - #[Test] - public function lookup_value_interface() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - - Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); - } + $r->out= $out; + $r->out->write("'"); - #[Test, Expect(ClassNotFoundException::class)] - public function lookup_non_existant() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->lookup('\\NotFound'); + Assert::equals("'echo \'Hello\''", $out->bytes()); } #[Test] public function line_number_initially_1() { - $r= new Result(new StringWriter(new MemoryOutputStream())); + $r= new Result(new MemoryOutputStream()); Assert::equals(1, $r->line); } - #[Test, Values([[1, 'at($line)->out->write('test'); Assert::equals($expected, $out->bytes()); @@ -122,10 +56,10 @@ public function write_at_line($line, $expected) { #[Test] public function at_cannot_go_backwards() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), 'at(0)->out->write('test'); - Assert::equals('bytes()); + Assert::equals('test', $out->bytes()); Assert::equals(1, $r->line); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php index 722aca93..620eed0a 100755 --- a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php @@ -1,7 +1,8 @@ type= $type; $this->emitter->emitOne($result, $node); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index d1073736..594f9532 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -1,7 +1,8 @@ language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); $out= new MemoryOutputStream(); - $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); return $out->bytes(); } @@ -75,8 +76,6 @@ protected function emit($code) { */ protected function type($code) { $name= 'T'.(self::$id++); - $out= new MemoryOutputStream(); - $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); if (isset($this->output['ast'])) { Console::writeLine(); @@ -84,7 +83,8 @@ protected function type($code) { Console::writeLine($tree); } - $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); + $out= new MemoryOutputStream(); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php new file mode 100755 index 00000000..2c91574b --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -0,0 +1,82 @@ +temp(), $r->temp(), $r->temp()]); + } + + #[Test] + public function prolog_and_epilog_default_to_emtpy_strings() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + Assert::equals('', $out->bytes()); + } + + #[Test, Values(['', 'close(); + Assert::equals($prolog, $out->bytes()); + } + + #[Test] + public function writes_epilog_on_closing() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out, ''); + $r->close(); + + Assert::equals('', $out->bytes()); + } + + #[Test] + public function lookup_self() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); + } + + #[Test] + public function lookup_parent() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); + + Assert::equals(new Reflection(Value::class), $r->lookup('parent')); + } + + #[Test] + public function lookup_named() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); + } + + #[Test] + public function lookup_value_interface() { + $r= new GeneratedCode(new MemoryOutputStream()); + + Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function lookup_non_existant() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->lookup('\\NotFound'); + } +} \ No newline at end of file From 6ba6343c167d58611ea6b2a15aec5fe998f6d5c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 16 Feb 2022 18:09:40 +0100 Subject: [PATCH 593/926] Fix PHP 7 compatibility --- src/main/php/lang/ast/emit/AttributesAsComments.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 2b3cd209..5282490b 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -33,13 +33,13 @@ protected function emitAnnotations($result, $annotations) { $line= $annotations->line; $result->out->write('#['); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["\n" => " "])); + $out= $result->out; + $result->out= new Escaping($out, ["\n" => " "]); foreach ($annotations->named as $annotation) { $this->emitOne($result, $annotation); $result->out->write(','); } - $result->out->redirect($out); + $result->out= $out; $result->out->write("]\n"); $result->line= $line + 1; From 00687835ee10dc54218af4943003c2dcb1ac0304 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 16 Feb 2022 18:12:53 +0100 Subject: [PATCH 594/926] Remove need for temporary variables --- src/main/php/lang/ast/emit/AttributesAsComments.class.php | 5 ++--- src/main/php/lang/ast/emit/Escaping.class.php | 4 ++++ src/main/php/lang/ast/emit/PHP.class.php | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 5282490b..8870790c 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -33,13 +33,12 @@ protected function emitAnnotations($result, $annotations) { $line= $annotations->line; $result->out->write('#['); - $out= $result->out; - $result->out= new Escaping($out, ["\n" => " "]); + $result->out= new Escaping($result->out, ["\n" => " "]); foreach ($annotations->named as $annotation) { $this->emitOne($result, $annotation); $result->out->write(','); } - $result->out= $out; + $result->out= $result->out->original(); $result->out->write("]\n"); $result->line= $line + 1; diff --git a/src/main/php/lang/ast/emit/Escaping.class.php b/src/main/php/lang/ast/emit/Escaping.class.php index cf3685fa..e096035f 100755 --- a/src/main/php/lang/ast/emit/Escaping.class.php +++ b/src/main/php/lang/ast/emit/Escaping.class.php @@ -21,4 +21,8 @@ public function flush() { public function close() { $this->target->close(); } + + public function original() { + return $this->target; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0e401e86..cf0c0628 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -463,8 +463,7 @@ protected function emitAnnotation($result, $annotation) { // Found first non-constant argument, enclose in `eval` $result->out->write('(eval: \''); - $out= $result->out; - $result->out= new Escaping($out, ["'" => "\\'", '\\' => '\\\\']); + $result->out= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); // If exactly one unnamed argument exists, emit its value directly if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { @@ -479,7 +478,7 @@ protected function emitAnnotation($result, $annotation) { $result->out->write(']'); } - $result->out= $out; + $result->out= $result->out->original(); $result->out->write('\')'); return; } From e3470da2fba4b80f2df34bec9edcdeea0be8db48 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 19:51:00 +0100 Subject: [PATCH 595/926] Move at() to GeneratedCode class --- src/main/php/lang/ast/Result.class.php | 17 ++++------- .../php/lang/ast/emit/GeneratedCode.class.php | 29 ++++++++++++++----- .../lang/ast/unittest/ResultTest.class.php | 26 ----------------- .../unittest/emit/GeneratedCodeTest.class.php | 26 +++++++++++++++++ 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 0b3e5175..e3a8978c 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -6,11 +6,9 @@ class Result implements Closeable { public $out; public $codegen; - public $line= 1; public $meta= []; public $locals= []; public $stack= []; - public $type= []; /** * Starts a result stream, including an optional prolog and epilog @@ -23,23 +21,20 @@ public function __construct(OutputStream $out) { } /** - * Forwards output line to given line number + * Finalize result. Guaranteed to be called *once* from within `close()`. + * Without implementation here - overwrite in subclasses. * - * @param int $line - * @return self + * @return void */ - public function at($line) { - if ($line > $this->line) { - $this->out->write(str_repeat("\n", $line - $this->line)); - $this->line= $line; - } - return $this; + protected function finalize() { + // NOOP } /** @return void */ public function close() { if (null === $this->out) return; + $this->finalize(); $this->out->close(); unset($this->out); } diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index f986b502..6d4fad51 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -4,6 +4,8 @@ class GeneratedCode extends Result { private $epilog; + public $line= 1; + public $type= []; /** * Starts a result stream, including an optional prolog and epilog @@ -19,6 +21,20 @@ public function __construct($out, $prolog= '', $epilog= '') { $this->epilog= $epilog; } + /** + * Forwards output line to given line number + * + * @param int $line + * @return self + */ + public function at($line) { + if ($line > $this->line) { + $this->out->write(str_repeat("\n", $line - $this->line)); + $this->line= $line; + } + return $this; + } + /** * Creates a temporary variable and returns its name * @@ -48,13 +64,12 @@ public function lookup($type) { return new Reflection($type); } - /** @return void */ - public function close() { - if (null === $this->out) return; - - // Write epilog, then close and ensure this doesn't happen twice + /** + * Write epilog + * + * @return void + */ + protected function finalize() { '' === $this->epilog || $this->out->write($this->epilog); - $this->out->close(); - unset($this->out); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 3ded288c..3847680c 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -36,30 +36,4 @@ public function write_escaped() { Assert::equals("'echo \'Hello\''", $out->bytes()); } - - #[Test] - public function line_number_initially_1() { - $r= new Result(new MemoryOutputStream()); - Assert::equals(1, $r->line); - } - - #[Test, Values([[1, 'test'], [2, "\ntest"], [3, "\n\ntest"]])] - public function write_at_line($line, $expected) { - $out= new MemoryOutputStream(); - $r= new Result($out); - $r->at($line)->out->write('test'); - - Assert::equals($expected, $out->bytes()); - Assert::equals($line, $r->line); - } - - #[Test] - public function at_cannot_go_backwards() { - $out= new MemoryOutputStream(); - $r= new Result($out); - $r->at(0)->out->write('test'); - - Assert::equals('test', $out->bytes()); - Assert::equals(1, $r->line); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 2c91574b..3d1ed780 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -79,4 +79,30 @@ public function lookup_non_existant() { $r= new GeneratedCode(new MemoryOutputStream()); $r->lookup('\\NotFound'); } + + #[Test] + public function line_number_initially_1() { + $r= new GeneratedCode(new MemoryOutputStream()); + Assert::equals(1, $r->line); + } + + #[Test, Values([[1, 'test'], [2, "\ntest"], [3, "\n\ntest"]])] + public function write_at_line($line, $expected) { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + $r->at($line)->out->write('test'); + + Assert::equals($expected, $out->bytes()); + Assert::equals($line, $r->line); + } + + #[Test] + public function at_cannot_go_backwards() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + $r->at(0)->out->write('test'); + + Assert::equals('test', $out->bytes()); + Assert::equals(1, $r->line); + } } \ No newline at end of file From 91850eb9364a8fa078fcf33aa82aa7019d57efcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 19:52:07 +0100 Subject: [PATCH 596/926] Move Result class to lang.ast.emit --- src/main/php/lang/ast/emit/GeneratedCode.class.php | 2 -- src/main/php/lang/ast/{ => emit}/Result.class.php | 3 ++- src/test/php/lang/ast/unittest/ResultTest.class.php | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) rename src/main/php/lang/ast/{ => emit}/Result.class.php (93%) diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 6d4fad51..2950ac45 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -1,7 +1,5 @@ Date: Thu, 17 Feb 2022 20:04:24 +0100 Subject: [PATCH 597/926] Move output line positioning to GeneratedCode class --- src/main/php/lang/ast/Emitter.class.php | 11 +++-------- src/main/php/lang/ast/emit/PHP.class.php | 11 +++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 2943bc76..148dd1e9 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -129,23 +129,17 @@ public function emitAll($result, $nodes) { */ public function emitOne($result, $node) { - // Inlined Result::at() - if ($node->line > $result->line) { - $result->out->write(str_repeat("\n", $node->line - $result->line)); - $result->line= $node->line; - } - // Check for transformations if (isset($this->transformations[$node->kind])) { foreach ($this->transformations[$node->kind] as $transformation) { $r= $transformation($result->codegen, $node); if ($r instanceof Node) { if ($r->kind === $node->kind) continue; - $this->{"emit{$r->kind}"}($result, $r); + $this->{'emit'.$r->kind}($result, $r); return; } else if ($r) { foreach ($r as $n) { - $this->{"emit{$n->kind}"}($result, $n); + $this->{'emit'.$n->kind}($result, $n); $result->out->write(';'); } return; @@ -153,6 +147,7 @@ public function emitOne($result, $node) { } // Fall through, use default } + $this->{'emit'.$node->kind}($result, $node); } diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index cf0c0628..e67ab4da 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1034,4 +1034,15 @@ protected function emitFrom($result, $from) { $result->out->write('yield from '); $this->emitOne($result, $from->iterable); } + + /** + * Emit single nodes + * + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @return void + */ + public function emitOne($result, $node) { + parent::emitOne($result->at($node->line), $node); + } } \ No newline at end of file From ba39d7f7a31a8a29fde8100ff461476a9e0e1134 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 20:12:28 +0100 Subject: [PATCH 598/926] Add Result::initialize() --- .../php/lang/ast/emit/GeneratedCode.class.php | 33 ++++++++++++------- src/main/php/lang/ast/emit/Result.class.php | 13 +++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 2950ac45..8fbe4d27 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -1,7 +1,7 @@ prolog= $prolog; + $this->epilog= $epilog; parent::__construct($out); + } - $out->write($prolog); - $this->epilog= $epilog; + /** + * Initialize result. Guaranteed to be called *once* from constructor. + * Without implementation here - overwrite in subclasses. + * + * @return void + */ + protected function initialize() { + '' === $this->prolog || $this->out->write($this->prolog); + } + + /** + * Write epilog + * + * @return void + */ + protected function finalize() { + '' === $this->epilog || $this->out->write($this->epilog); } /** @@ -61,13 +79,4 @@ public function lookup($type) { return new Reflection($type); } - - /** - * Write epilog - * - * @return void - */ - protected function finalize() { - '' === $this->epilog || $this->out->write($this->epilog); - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php index 8562d688..a9b7dd58 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -12,13 +12,24 @@ class Result implements Closeable { public $stack= []; /** - * Starts a result stream, including an optional prolog and epilog + * Starts a result stream. * * @param io.streams.OutputStream $out */ public function __construct(OutputStream $out) { $this->out= $out; $this->codegen= new CodeGen(); + $this->initialize(); + } + + /** + * Initialize result. Guaranteed to be called *once* from constructor. + * Without implementation here - overwrite in subclasses. + * + * @return void + */ + protected function initialize() { + // NOOP } /** From 65e20143a65571873d87f4bc1fbf52f3629c2061 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Mar 2022 14:28:51 +0100 Subject: [PATCH 599/926] Ignore well-known PHP files that are not types --- .../lang/ast/CompilingClassloader.class.php | 8 +++- .../loader/CompilingClassLoaderTest.class.php | 47 ++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index b8f04102..ee047701 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -16,6 +16,7 @@ class CompilingClassLoader implements IClassLoader { const EXTENSION = '.php'; + private static $ignore= ['autoload.php' => true, '__xp.php' => true]; private static $instance= []; private $version; private $source= []; @@ -60,7 +61,9 @@ protected function locateSource($class) { */ public function providesUri($uri) { if (isset($this->source[$uri])) return true; - if (0 !== substr_compare($uri, self::EXTENSION, -4)) return false; + + $e= -strlen(self::EXTENSION); + if (0 !== substr_compare($uri, self::EXTENSION, $e)) return false; if (0 === substr_compare($uri, \xp::CLASS_FILE_EXT, -strlen(\xp::CLASS_FILE_EXT))) return false; foreach (ClassLoader::getDefault()->getLoaders() as $loader) { @@ -68,7 +71,7 @@ public function providesUri($uri) { $l= strlen($loader->path); if (0 === substr_compare($loader->path, $uri, 0, $l)) { - $this->source[$uri]= strtr(substr($uri, $l, -4), [DIRECTORY_SEPARATOR => '.']); + $this->source[$uri]= strtr(substr($uri, $l, $e), [DIRECTORY_SEPARATOR => '.']); return true; } } @@ -117,6 +120,7 @@ public function packageContents($package) { foreach (ClassLoader::getDefault()->getLoaders() as $loader) { if ($loader instanceof self) continue; foreach ($loader->packageContents($package) as $content) { + if (isset(self::$ignore[$content])) continue; if (self::EXTENSION === substr($content, $p= strpos($content, '.'))) { $r[]= substr($content, 0, $p).\xp::CLASS_FILE_EXT; } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 9132a223..caf47301 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -13,6 +13,20 @@ static function __static() { self::$runtime= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; } + private function tempFolder($structure) { + $namespace= 'ns'.uniqid(); + $folder= new Folder(Environment::tempDir(), $namespace); + $folder->exists() || $folder->create(); + + $names= []; + foreach ($structure as $type => $code) { + Files::write(new File($folder, $type.'.php'), sprintf($code, $namespace)); + $names[$type]= $namespace.'.'.$type; + } + + return [$folder, $names]; + } + /** * Sets us compiling class loader with a given type and source code, then * executes callback. @@ -22,20 +36,11 @@ static function __static() { * @return var */ private function compile($source, $callback) { - $namespace= 'ns'.uniqid(); - $folder= new Folder(Environment::tempDir(), $namespace); - $folder->exists() || $folder->create(); + list($folder, $names)= $this->tempFolder($source); - $names= []; - foreach ($source as $type => $code) { - Files::write(new File($folder, $type.'.php'), sprintf($code, $namespace)); - $names[$type]= $namespace.'.'.$type; - } $cl= ClassLoader::registerPath($folder->path); - - $loader= CompilingClassLoader::instanceFor(self::$runtime); try { - return $callback($loader, $names, $cl); + return $callback(CompilingClassLoader::instanceFor(self::$runtime), $names, $cl); } finally { ClassLoader::removeLoader($cl); $folder->unlink(); @@ -197,4 +202,24 @@ public function loading_non_existant_resource() { public function loading_non_existant_resource_as_stream() { CompilingClassLoader::instanceFor(self::$runtime)->getResourceAsStream('notfound.md'); } + + #[Test] + public function ignores_autoload_and_xp_entry() { + list($folder, $names)= $this->tempFolder([ + '__xp' => ' ' 'path); + try { + Assert::equals( + ['Fixture.class.php'], + CompilingClassLoader::instanceFor(self::$runtime)->packageContents($folder->dirname) + ); + } finally { + ClassLoader::removeLoader($cl); + $folder->unlink(); + } + } } \ No newline at end of file From 9586a464e02ec0c0ded50a3ae2375c87bc99998c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Mar 2022 14:30:26 +0100 Subject: [PATCH 600/926] QA: Use "-" inside substr() to clarify intent --- src/main/php/lang/ast/CompilingClassloader.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index ee047701..5b70fbd8 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -62,8 +62,8 @@ protected function locateSource($class) { public function providesUri($uri) { if (isset($this->source[$uri])) return true; - $e= -strlen(self::EXTENSION); - if (0 !== substr_compare($uri, self::EXTENSION, $e)) return false; + $e= strlen(self::EXTENSION); + if (0 !== substr_compare($uri, self::EXTENSION, -$e)) return false; if (0 === substr_compare($uri, \xp::CLASS_FILE_EXT, -strlen(\xp::CLASS_FILE_EXT))) return false; foreach (ClassLoader::getDefault()->getLoaders() as $loader) { @@ -71,7 +71,7 @@ public function providesUri($uri) { $l= strlen($loader->path); if (0 === substr_compare($loader->path, $uri, 0, $l)) { - $this->source[$uri]= strtr(substr($uri, $l, $e), [DIRECTORY_SEPARATOR => '.']); + $this->source[$uri]= strtr(substr($uri, $l, -$e), [DIRECTORY_SEPARATOR => '.']); return true; } } From bfc61a4f4861c765c50c4081ca26dc2aed47a7ff Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Mar 2022 14:47:13 +0100 Subject: [PATCH 601/926] Remove Travis CI --- .travis.yml | 23 ----------------------- README.md | 1 - 2 files changed, 24 deletions(-) delete mode 100755 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index a19f0401..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -# xp-framework/compiler - -language: php - -php: - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - master - -matrix: - allow_failures: - - php: master - -before_script: - - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.1.sh > xp-run - - composer install --prefer-dist - - echo "vendor/autoload.php" > composer.pth - -script: - - sh xp-run xp.unittest.TestRunner src/test/php diff --git a/README.md b/README.md index 91c960e9..5ac11037 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ XP Compiler =========== [![Build status on GitHub](https://github.com/xp-framework/compiler/workflows/Tests/badge.svg)](https://github.com/xp-framework/compiler/actions) -[![Build Status on TravisCI](https://secure.travis-ci.org/xp-forge/sequence.svg)](http://travis-ci.org/xp-framework/compiler) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) [![Requires PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.svg)](http://php.net/) From b81c22beab4709246af3391dc3460859ac728939 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 7 Mar 2022 13:15:22 +0100 Subject: [PATCH 602/926] Synchronize line numbers when emitting comments Fixes #136 --- ChangeLog.md | 3 +++ src/main/php/lang/ast/emit/PHP.class.php | 1 + .../lang/ast/unittest/EmitterTest.class.php | 22 ++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index aa6f1e31..3ea419f2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed #136: Line number inconsistent after multi-line doc comments + (@thekid) + ## 8.2.0 / 2022-01-30 * Support passing emitter-augmenting class names to the instanceFor() diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index c0971156..d7d7728f 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -441,6 +441,7 @@ protected function emitMeta($result, $name, $annotations, $comment) { protected function emitComment($result, $comment) { $result->out->write($comment->declaration); + $result->line+= substr_count($comment->declaration, "\n"); } protected function emitAnnotation($result, $annotation) { diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 76844908..a6b0ea27 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -1,7 +1,7 @@ bytes()); } + + #[Test] + public function emit_multiline_comment() { + $fixture= $this->newEmitter(); + $out= new MemoryOutputStream(); + $fixture->emitAll(new Result($out), [ + new Comment( + "/**\n". + " * Doc comment\n". + " *\n". + " * @see http://example.com/\n". + " */", + 3 + ), + new Variable('a', 8) + ]); + + $code= $out->bytes(); + Assert::equals('$a;', explode("\n", $code)[7], $code); + } } \ No newline at end of file From d6e74bfd9e9f0a22487ab0f3f941c5273b9543b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 7 Mar 2022 13:17:10 +0100 Subject: [PATCH 603/926] Release 8.3.0 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3ea419f2..e750e3e8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.3.0 / 2022-03-07 + +* Made JIT class loader ignore *autoload.php* files - @thekid * Fixed #136: Line number inconsistent after multi-line doc comments (@thekid) From 4936334345745d41a4713e88248ea8f02095f91d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 3 Apr 2022 12:01:15 +0200 Subject: [PATCH 604/926] Remove class testing functionality from xp-framework/ast --- .../php/lang/ast/unittest/ScopeTest.class.php | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100755 src/test/php/lang/ast/unittest/ScopeTest.class.php diff --git a/src/test/php/lang/ast/unittest/ScopeTest.class.php b/src/test/php/lang/ast/unittest/ScopeTest.class.php deleted file mode 100755 index 81d8aa69..00000000 --- a/src/test/php/lang/ast/unittest/ScopeTest.class.php +++ /dev/null @@ -1,76 +0,0 @@ -package('test'); - - Assert::equals('\\test', $s->package); - } - - #[Test] - public function resolve_in_global_scope() { - $s= new Scope(); - - Assert::equals('\\Parse', $s->resolve('Parse')); - } - - #[Test] - public function resolve_in_package() { - $s= new Scope(); - $s->package('test'); - - Assert::equals('\\test\\Parse', $s->resolve('Parse')); - } - - #[Test] - public function resolve_relative_in_package() { - $s= new Scope(); - $s->package('test'); - - Assert::equals('\\test\\ast\\Parse', $s->resolve('ast\\Parse')); - } - - #[Test] - public function resolve_imported_in_package() { - $s= new Scope(); - $s->package('test'); - $s->import('lang\\ast\\Parse'); - - Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); - } - - #[Test] - public function resolve_imported_in_global_scope() { - $s= new Scope(); - $s->import('lang\\ast\\Parse'); - - Assert::equals('\\lang\\ast\\Parse', $s->resolve('Parse')); - } - - #[Test] - public function package_inherited_from_parent() { - $s= new Scope(); - $s->package('test'); - - Assert::equals('\\test\\Parse', (new Scope($s))->resolve('Parse')); - } - - #[Test] - public function import_inherited_from_parent() { - $s= new Scope(); - $s->import('lang\\ast\\Parse'); - - Assert::equals('\\lang\\ast\\Parse', (new Scope($s))->resolve('Parse')); - } -} \ No newline at end of file From 91e41b09517bb05777d90a72b5d2e8b7b085c043 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 3 Apr 2022 12:13:36 +0200 Subject: [PATCH 605/926] Add various namespaces tests --- .../unittest/emit/NamespacesTest.class.php | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php new file mode 100755 index 00000000..0da996aa --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php @@ -0,0 +1,87 @@ +type('class { }')->getPackage()->getName()); + } + + #[Test] + public function with_namespace() { + Assert::equals('test', $this->type('namespace test; class { }')->getPackage()->getName()); + } + + #[Test] + public function resolves_unqualified() { + $r= $this->run('namespace util; class { + public function run() { + return new Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_relative() { + $r= $this->run('class { + public function run() { + return new util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_absolute() { + $r= $this->run('namespace test; class { + public function run() { + return new \util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_import() { + $r= $this->run('namespace test; use util\Date; class { + public function run() { + return new Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_alias() { + $r= $this->run('namespace test; use util\Date as DateTime; class { + public function run() { + return new DateTime("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_namespace_keyword() { + $r= $this->run('namespace util; class { + public function run() { + return new namespace\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_sub_namespace() { + $r= $this->run('class { + public function run() { + return new namespace\util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } +} \ No newline at end of file From 8a1cc74ddd67d31fb307c3d9cb26adf39ab0894e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 May 2022 21:36:56 +0200 Subject: [PATCH 606/926] Implement readonly modifier for classes See https://wiki.php.net/rfc/readonly_classes --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP70.class.php | 1 + src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 3 ++- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- src/main/php/lang/ast/emit/PHP82.class.php | 2 +- .../lang/ast/emit/ReadonlyClasses.class.php | 21 +++++++++++++++++++ ...sTest.class.php => ReadonlyTest.class.php} | 19 ++++++++++++++--- 10 files changed, 46 insertions(+), 7 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ReadonlyClasses.class.php rename src/test/php/lang/ast/unittest/emit/{ReadonlyPropertiesTest.class.php => ReadonlyTest.class.php} (90%) diff --git a/composer.json b/composer.json index 429acfec..65632929 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": "^8.0", + "xp-framework/ast": "dev-feature/readonly_classes as 8.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 6962bb4b..5d7be79e 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -19,6 +19,7 @@ class PHP70 extends PHP { OmitArgumentNames, OmitConstModifiers, OmitPropertyTypes, + ReadonlyClasses, ReadonlyProperties, RewriteClassOnObjects, RewriteEnums, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 4aabf251..8ac6ba0f 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -19,6 +19,7 @@ class PHP71 extends PHP { OmitArgumentNames, OmitPropertyTypes, ReadonlyProperties, + ReadonlyClasses, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 2c9aa978..23a0bdc6 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -19,6 +19,7 @@ class PHP72 extends PHP { OmitArgumentNames, OmitPropertyTypes, ReadonlyProperties, + ReadonlyClasses, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index bf7c1e87..25e9a286 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -17,6 +17,7 @@ class PHP74 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + ReadonlyClasses, ReadonlyProperties, RewriteBlockLambdaExpressions, RewriteClassOnObjects, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 779c224c..af4026a6 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,8 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; + use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums; + use ReadonlyClasses, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 1317fc28..4cc19ab2 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, ReadonlyClasses; /** 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 df654511..78dc8370 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { - use RewriteBlockLambdaExpressions; + use RewriteBlockLambdaExpressions, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php new file mode 100755 index 00000000..832f43e7 --- /dev/null +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -0,0 +1,21 @@ +modifiers))) { + unset($class->modifiers[$p]); + foreach ($class->body as $member) { + if ($member->is('property')) $member->modifiers[]= 'readonly'; + } + } + + return parent::emitClass($result, $class); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php similarity index 90% rename from src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php rename to src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 2f65771c..34fe9dbb 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyPropertiesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -4,11 +4,12 @@ use unittest\{Assert, Expect, Test}; /** - * Readonly properties + * Readonly classes and properties * * @see https://wiki.php.net/rfc/readonly_properties_v2 + * @see https://wiki.php.net/rfc/readonly_classes */ -class ReadonlyPropertiesTest extends EmittingTest { +class ReadonlyTest extends EmittingTest { /** @return iterable */ private function modifiers() { @@ -20,7 +21,19 @@ private function modifiers() { } #[Test] - public function declaration() { + public function class_declaration() { + $t= $this->type('readonly class { + public int $fixture; + }'); + + Assert::equals( + sprintf('public readonly int %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); + } + + #[Test] + public function property_declaration() { $t= $this->type('class { public readonly int $fixture; }'); From a57bc102e805acb2999f05a5f0b802c2c2a84e21 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 May 2022 21:47:59 +0200 Subject: [PATCH 607/926] Inherit readonly on class to promoted constructor parameters --- .../lang/ast/emit/ReadonlyClasses.class.php | 11 +++++++++-- .../ast/unittest/emit/ReadonlyTest.class.php | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index 832f43e7..65a96da5 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -2,7 +2,8 @@ /** * Implements readonly properties by removing the `readonly` modifier from - * the class and inheriting it to all properties. + * the class and inheriting it to all properties (and promoted constructor + * arguments). * * @see https://wiki.php.net/rfc/readonly_classes */ @@ -12,7 +13,13 @@ protected function emitClass($result, $class) { if (false !== ($p= array_search('readonly', $class->modifiers))) { unset($class->modifiers[$p]); foreach ($class->body as $member) { - if ($member->is('property')) $member->modifiers[]= 'readonly'; + if ($member->is('property')) { + $member->modifiers[]= 'readonly'; + } else if ($member->is('method')) { + foreach ($member->signature->parameters as $param) { + $param->promote && $param->promote.= ' readonly'; + } + } } } diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 34fe9dbb..72b6a101 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -45,11 +45,28 @@ public function property_declaration() { } #[Test] - public function with_constructor_argument_promotion() { + public function class_with_constructor_argument_promotion() { + $t= $this->type('readonly class { + public function __construct(public string $fixture) { } + }'); + + Assert::equals( + sprintf('public readonly string %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function property_defined_with_constructor_argument_promotion() { $t= $this->type('class { public function __construct(public readonly string $fixture) { } }'); + Assert::equals( + sprintf('public readonly string %s::$fixture', $t->getName()), + $t->getField('fixture')->toString() + ); Assert::equals('Test', $t->newInstance('Test')->fixture); } From 59691aee226771ae3229c13b7a85b46d7d1f9e6f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 May 2022 22:21:47 +0200 Subject: [PATCH 608/926] Prevent dynamic members on readonly classes --- src/main/php/lang/ast/emit/PHP.class.php | 14 +++++++++----- .../php/lang/ast/emit/ReadonlyClasses.class.php | 8 ++++++++ .../lang/ast/unittest/emit/ReadonlyTest.class.php | 12 ++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d7d7728f..2544977a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -383,7 +383,7 @@ protected function emitEnum($result, $enum) { protected function emitClass($result, $class) { array_unshift($result->type, $class); array_unshift($result->meta, []); - $result->locals= [[], [], []]; + $result->locals ?: $result->locals= [[], [], []]; $class->comment && $this->emitOne($result, $class->comment); $class->annotations && $this->emitOne($result, $class->annotations); @@ -399,21 +399,24 @@ protected function emitClass($result, $class) { if ($result->locals[2]) { $result->out->write('private $__virtual= ['); foreach ($result->locals[2] as $name => $access) { - $result->out->write("'{$name}' => null,"); + $name && $result->out->write("'{$name}' => null,"); } $result->out->write('];'); $result->out->write('public function __get($name) { switch ($name) {'); foreach ($result->locals[2] as $name => $access) { - $result->out->write('case "'.$name.'":'); + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[0]); $result->out->write('break;'); } - $result->out->write('default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); }}'); + isset($result->locals[2][null]) || $result->out->write( + 'default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING);' + ); + $result->out->write('}}'); $result->out->write('public function __set($name, $value) { switch ($name) {'); foreach ($result->locals[2] as $name => $access) { - $result->out->write('case "'.$name.'":'); + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[1]); $result->out->write('break;'); } @@ -433,6 +436,7 @@ protected function emitClass($result, $class) { $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name.'::__init();'); array_shift($result->type); + $result->locals= []; } protected function emitMeta($result, $name, $annotations, $comment) { diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index 65a96da5..ab29f97f 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -1,5 +1,7 @@ modifiers))) { unset($class->modifiers[$p]); + + // Inherit foreach ($class->body as $member) { if ($member->is('property')) { $member->modifiers[]= 'readonly'; @@ -21,6 +25,10 @@ protected function emitClass($result, $class) { } } } + + // Prevent dynamic members + $throw= new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);'); + $result->locals= [[], [], [null => [$throw, $throw]]]; } return parent::emitClass($result, $class); diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 72b6a101..bb86f0ae 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -173,4 +173,16 @@ public function cannot_have_an_initial_value() { public readonly string $fixture= "Test"; }'); } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + public function cannot_read_dynamic_members_from_readonly_classes() { + $t= $this->type('readonly class { }'); + $t->newInstance()->fixture; + } + + #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + public function cannot_write_dynamic_members_from_readonly_classes() { + $t= $this->type('readonly class { }'); + $t->newInstance()->fixture= true; + } } \ No newline at end of file From 72dfff60a1982a4390db63ee62cd56e8236254dc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 May 2022 22:26:07 +0200 Subject: [PATCH 609/926] Add test for static properties --- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index bb86f0ae..f13ae5a5 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -185,4 +185,11 @@ public function cannot_write_dynamic_members_from_readonly_classes() { $t= $this->type('readonly class { }'); $t->newInstance()->fixture= true; } + + #[Test, Ignore('Until proper error handling facilities exist')] + public function readonly_classes_cannot_have_static_members() { + $this->type('readonly class { + public static $test; + }'); + } } \ No newline at end of file From 192ac0948a2ed732797039206adabae8bacfd3cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 7 May 2022 12:11:13 +0200 Subject: [PATCH 610/926] MFH --- .../lang/ast/unittest/EmitterTest.class.php | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 87b1a7fc..347ff56e 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -109,19 +109,20 @@ public function transform_to_null() { #[Test] public function emit_multiline_comment() { - $fixture= $this->newEmitter(); - $out= new MemoryOutputStream(); - $fixture->emitAll(new Result($out), [ - new Comment( - "/**\n". - " * Doc comment\n". - " *\n". - " * @see http://example.com/\n". - " */", - 3 - ), - new Variable('a', 8) - ]); + $out= $this->newEmitter()->write( + [ + new Comment( + "/**\n". + " * Doc comment\n". + " *\n". + " * @see http://example.com/\n". + " */", + 3 + ), + new Variable('a', 8) + ], + new MemoryOutputStream() + ); $code= $out->bytes(); Assert::equals('$a;', explode("\n", $code)[7], $code); From f5d8e528c123ff27444cc97753f41abac97a7078 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 7 May 2022 12:38:58 +0200 Subject: [PATCH 611/926] Document result creation refactoring --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index e750e3e8..8a042cb4 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #135: Move responsibility for creating result to emitter + (@thekid) + ## 8.3.0 / 2022-03-07 * Made JIT class loader ignore *autoload.php* files - @thekid From 06fa65bdb89a1fd4b252e7115c253205d000852c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 May 2022 17:03:42 +0200 Subject: [PATCH 612/926] Upgrade to release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 65632929..d36e9284 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": "dev-feature/readonly_classes as 8.1.0", + "xp-framework/ast": "^8.1", "php" : ">=7.0.0" }, "require-dev" : { From 790dae04ef29e092804632f9b6b531cea19015e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 14 May 2022 17:11:01 +0200 Subject: [PATCH 613/926] Release 8.4.0 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 8a042cb4..7eb4c03c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.4.0 / 2022-05-14 + +* Merged PR #138: Implement readonly modifier for classes - a PHP 8.2 + feature, see https://wiki.php.net/rfc/readonly_classes + (@thekid) * Merged PR #135: Move responsibility for creating result to emitter (@thekid) From 994ac2c99834d22a5137d9d23677ff07cd9b172b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Jun 2022 11:06:58 +0200 Subject: [PATCH 614/926] Add support for null, true and false types See https://wiki.php.net/rfc/null-false-standalone-types See https://wiki.php.net/rfc/true-type --- src/main/php/lang/ast/emit/PHP70.class.php | 4 +- src/main/php/lang/ast/emit/PHP71.class.php | 4 +- src/main/php/lang/ast/emit/PHP72.class.php | 4 +- src/main/php/lang/ast/emit/PHP74.class.php | 4 +- src/main/php/lang/ast/emit/PHP80.class.php | 4 +- src/main/php/lang/ast/emit/PHP81.class.php | 7 +++- .../ast/unittest/TypeLiteralsTest.class.php | 41 ++++++++++++++++++- 7 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 5d7be79e..49c013d8 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -41,8 +41,10 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { + static $omit= ['object' => 1, 'void' => 1, 'iterable' => 1, 'mixed' => 1, 'never' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + $l= $t->literal(); - return ('object' === $l || 'void' === $l || 'iterable' === $l || 'mixed' === $l || 'never' === $l) ? null : $l; + return isset($omit[$l]) ? null : $l; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 8ac6ba0f..25d9e991 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -39,8 +39,10 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { + static $omit= ['object' => 1, 'mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + $l= $t->literal(); - return ('object' === $l || 'mixed' === $l) ? null : ('never' === $l ? 'void' : $l); + return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 23a0bdc6..6175f8a8 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -39,8 +39,10 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { + static $omit= ['mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + $l= $t->literal(); - return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); + return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 25e9a286..6461a297 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -37,8 +37,10 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { + static $omit= ['mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + $l= $t->literal(); - return 'mixed' === $l ? null : ('never' === $l ? 'void' : $l); + return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index af4026a6..73c66738 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -30,8 +30,10 @@ public function __construct() { return substr($u, 1); }, IsLiteral::class => function($t) { + static $omit= ['true' => 1, 'false' => 1, 'null' => 1]; + $l= $t->literal(); - return 'never' === $l ? 'void' : $l; + return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); } ]; } diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 4cc19ab2..c95cf4ab 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -36,7 +36,12 @@ public function __construct() { } return substr($u, 1); }, - IsLiteral::class => function($t) { return $t->literal(); } + IsLiteral::class => function($t) { + static $omit= ['true' => 1, 'false' => 1, 'null' => 1]; + + $l= $t->literal(); + return isset($omit[$l]) ? null : $l; + } ]; } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index b176a07b..08d3f52f 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -27,8 +27,13 @@ private function php70() { yield [new IsLiteral('never'), null]; yield [new IsLiteral('iterable'), null]; yield [new IsLiteral('mixed'), null]; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), null]; + yield [new IsLiteral('true'), null]; yield [new IsNullable(new IsLiteral('string')), null]; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } /** @@ -43,9 +48,14 @@ private function php71() { yield [new IsLiteral('never'), 'void']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), null]; + yield [new IsLiteral('true'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), null]; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } /** @@ -60,9 +70,14 @@ private function php72() { yield [new IsLiteral('never'), 'void']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), null]; + yield [new IsLiteral('true'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } /** @@ -86,9 +101,14 @@ private function php80() { yield [new IsLiteral('never'), 'void']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), null]; + yield [new IsLiteral('true'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } /** @@ -103,19 +123,36 @@ private function php81() { yield [new IsLiteral('never'), 'never']; yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), null]; + yield [new IsLiteral('true'), null]; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; } /** - * PHP 8.2 is the same as PHP 8.1 + * PHP 8.2 added `null`, `false` and `true` * * @return iterable */ private function php82() { - yield from $this->php81(); + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'never']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), 'null']; + yield [new IsLiteral('false'), 'false']; + yield [new IsLiteral('true'), 'true']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|false']; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; } #[Test, Values('php70')] From b1cc4921f5d280c009bacc74054feb1b6d897d58 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 20 Jul 2022 23:15:29 +0200 Subject: [PATCH 615/926] Rewrite `true` and `false` to `bool` See https://github.com/xp-framework/compiler/pull/140#issuecomment-1152886659 --- src/main/php/lang/ast/emit/PHP70.class.php | 13 ++++++++-- src/main/php/lang/ast/emit/PHP71.class.php | 11 +++++++-- src/main/php/lang/ast/emit/PHP72.class.php | 10 ++++++-- src/main/php/lang/ast/emit/PHP74.class.php | 10 ++++++-- src/main/php/lang/ast/emit/PHP80.class.php | 9 +++++-- src/main/php/lang/ast/emit/PHP81.class.php | 8 +++++-- .../ast/unittest/TypeLiteralsTest.class.php | 24 +++++++++---------- 7 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 49c013d8..2a6cca27 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -41,10 +41,19 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { - static $omit= ['object' => 1, 'void' => 1, 'iterable' => 1, 'mixed' => 1, 'never' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'object' => 1, + 'void' => 1, + 'iterable' => 1, + 'mixed' => 1, + 'null' => 1, + 'never' => 1, + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : $l; + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 25d9e991..9b0f98a6 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -39,10 +39,17 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { - static $omit= ['object' => 1, 'mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'object' => 1, + 'mixed' => 1, + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 6175f8a8..6e533036 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -39,10 +39,16 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { - static $omit= ['mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'mixed' => 1, + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 6461a297..538d9f40 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -37,10 +37,16 @@ public function __construct() { IsUnion::class => function($t) { return null; }, IsIntersection::class => function($t) { return null; }, IsLiteral::class => function($t) { - static $omit= ['mixed' => 1, 'true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'mixed' => 1, + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, ]; } diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 73c66738..bf6394b6 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -30,10 +30,15 @@ public function __construct() { return substr($u, 1); }, IsLiteral::class => function($t) { - static $omit= ['true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : ('never' === $l ? 'void' : $l); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; } ]; } diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index c95cf4ab..4eba166a 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -37,10 +37,14 @@ public function __construct() { return substr($u, 1); }, IsLiteral::class => function($t) { - static $omit= ['true' => 1, 'false' => 1, 'null' => 1]; + static $rewrite= [ + 'null' => 1, + 'true' => 'bool', + 'false' => 'bool', + ]; $l= $t->literal(); - return isset($omit[$l]) ? null : $l; + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; } ]; } diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index 08d3f52f..37036a40 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -28,8 +28,8 @@ private function php70() { yield [new IsLiteral('iterable'), null]; yield [new IsLiteral('mixed'), null]; yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), null]; - yield [new IsLiteral('true'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; yield [new IsNullable(new IsLiteral('string')), null]; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; @@ -49,8 +49,8 @@ private function php71() { yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), null]; - yield [new IsLiteral('true'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), null]; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; @@ -71,8 +71,8 @@ private function php72() { yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), null]; yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), null]; - yield [new IsLiteral('true'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; @@ -102,12 +102,12 @@ private function php80() { yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), 'mixed']; yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), null]; - yield [new IsLiteral('true'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|bool']; yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } @@ -124,12 +124,12 @@ private function php81() { yield [new IsLiteral('iterable'), 'iterable']; yield [new IsLiteral('mixed'), 'mixed']; yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), null]; - yield [new IsLiteral('true'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; yield [new IsNullable(new IsLiteral('string')), '?string']; yield [new IsNullable(new IsLiteral('object')), '?object']; yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|bool']; yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; } From c2ed168b70e0ecaf3a50e635fd4acb05c59aca58 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 20 Jul 2022 23:20:07 +0200 Subject: [PATCH 616/926] Release 8.5.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 7eb4c03c..7bebcf5a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.5.0 / 2022-07-20 + +* Merged PR #140: Add support for null, true and false types - @thekid + ## 8.4.0 / 2022-05-14 * Merged PR #138: Implement readonly modifier for classes - a PHP 8.2 From b6d14f6c7daebd7d6688c1b667e53dc2302817c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 13:16:17 +0200 Subject: [PATCH 617/926] Fix inconsistency between PHP 8.2 and PHP < 8.2 See https://github.com/xp-framework/compiler/issues/142#issuecomment-1236094837 --- src/main/php/lang/ast/emit/PHP80.class.php | 9 +++++++-- src/main/php/lang/ast/emit/PHP81.class.php | 9 +++++++-- .../ast/unittest/emit/UnionTypesTest.class.php | 14 +++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index bf6394b6..3eaab8bb 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -24,8 +24,13 @@ public function __construct() { IsUnion::class => function($t) { $u= ''; foreach ($t->components as $component) { - if (null === ($l= $this->literal($component))) return null; - $u.= '|'.$l; + if ('null' === $component->literal) { + $u.= '|null'; + } else if (null !== ($l= $this->literal($component))) { + $u.= '|'.$l; + } else { + return null; // One of the components didn't resolve + } } return substr($u, 1); }, diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 4eba166a..42b43c1d 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -31,8 +31,13 @@ public function __construct() { IsUnion::class => function($t) { $u= ''; foreach ($t->components as $component) { - if (null === ($l= $this->literal($component))) return null; - $u.= '|'.$l; + if ('null' === $component->literal) { + $u.= '|null'; + } else if (null !== ($l= $this->literal($component))) { + $u.= '|'.$l; + } else { + return null; // One of the components didn't resolve + } } return substr($u, 1); }, diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index 96d16476..33510f33 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -1,6 +1,6 @@ =8.0.0-dev")')] + public function nullable_union_type_restriction() { + $t= $this->type('class { + public function test(): int|string|null { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->getMethod('test')->getTypeRestriction() + ); + } + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] public function parameter_type_restriction_with_php8() { $t= $this->type('class { From 9555e485967ae4c2158dae2202e2cda3662175a5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 13:39:22 +0200 Subject: [PATCH 618/926] Fix tests --- src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index 33510f33..08728242 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -55,7 +55,7 @@ public function test(): int|string|null { } Assert::equals( new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), - $t->getMethod('test')->getTypeRestriction() + $t->getMethod('test')->getReturnTypeRestriction() ); } From 13d093d6704f1ff7f03e343a7cf31536d4d63716 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 13:55:37 +0200 Subject: [PATCH 619/926] Make test for backed enums work consistently for all PHP versions --- .../php/lang/ast/emit/RewriteEnums.class.php | 2 +- .../lang/ast/unittest/emit/EnumTest.class.php | 26 +++++++------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php index 713a5d61..cb6c059a 100755 --- a/src/main/php/lang/ast/emit/RewriteEnums.class.php +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -40,7 +40,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum ".self::class); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 8af47e5e..7c15a682 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -1,6 +1,7 @@ getMethod('from')->invoke(null, [$arg])->name); } - #[Test] + #[Test, Expect(class: Error::class, withMessage: '/"illegal" is not a valid backing value for enum .+/')] public function backed_enum_from_nonexistant() { - $t= $this->type('use lang\IllegalStateException; enum : string { + $t= $this->type('enum : string { case ASC = "asc"; case DESC = "desc"; - - public static function run() { - try { - self::from("illegal"); - throw new IllegalStateException("No exception raised"); - } catch (\Error $expected) { - return $expected->getMessage(); - } - } }'); - - Assert::equals( - '"illegal" is not a valid backing value for enum "'.$t->literal().'"', - $t->getMethod('run')->invoke(null) - ); + try { + $t->getMethod('from')->invoke(null, ['illegal']); + } catch (TargetInvocationException $e) { + throw $e->getCause(); + } } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] From 50258ee2461d97aa7ec5552391fdfa9f45aeb674 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 13:56:42 +0200 Subject: [PATCH 620/926] Verify nullable type unions are always yielded as lang.Nullable See https://github.com/xp-framework/compiler/issues/142#issuecomment-1236099698 --- .../lang/ast/unittest/emit/UnionTypesTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index 08728242..ffcaabc8 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -47,6 +47,18 @@ public function test(): int|string { } ); } + #[Test] + public function nullable_union_type() { + $t= $this->type('class { + public function test(): int|string|null { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->getMethod('test')->getReturnType() + ); + } + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] public function nullable_union_type_restriction() { $t= $this->type('class { From 6b6339174f7b584f988acdfe83dc79f9681f7391 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 14:01:39 +0200 Subject: [PATCH 621/926] Rewrite `?(int|string)` to `int|string|null` See #142 --- src/main/php/lang/ast/emit/PHP80.class.php | 5 ++++- src/main/php/lang/ast/emit/PHP81.class.php | 5 ++++- src/main/php/lang/ast/emit/PHP82.class.php | 5 ++++- .../lang/ast/unittest/emit/UnionTypesTest.class.php | 12 ++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 3eaab8bb..37fdfc25 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -19,7 +19,10 @@ public function __construct() { IsMap::class => function($t) { return 'array'; }, IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, IsIntersection::class => function($t) { return null; }, IsUnion::class => function($t) { $u= ''; diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 42b43c1d..eba47ef5 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -19,7 +19,10 @@ public function __construct() { IsMap::class => function($t) { return 'array'; }, IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, IsIntersection::class => function($t) { $i= ''; foreach ($t->components as $component) { diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 78dc8370..17dd3cb6 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -19,7 +19,10 @@ public function __construct() { IsMap::class => function($t) { return 'array'; }, IsFunction::class => function($t) { return 'callable'; }, IsValue::class => function($t) { return $t->literal(); }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, IsIntersection::class => function($t) { $i= ''; foreach ($t->components as $component) { diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index ffcaabc8..91772793 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -59,6 +59,18 @@ public function test(): int|string|null { } ); } + #[Test] + public function nullable_union_type_alternative_syntax() { + $t= $this->type('class { + public function test(): ?(int|string) { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->getMethod('test')->getReturnType() + ); + } + #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] public function nullable_union_type_restriction() { $t= $this->type('class { From b94c6c42800ed284e901af808ac9c0c9248a4637 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Sep 2022 14:04:08 +0200 Subject: [PATCH 622/926] Release 8.5.1 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 7bebcf5a..2a86de86 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.5.1 / 2022-09-03 + +* Fixed issue #142: Nullable type unions don't work as expected - @thekid + ## 8.5.0 / 2022-07-20 * Merged PR #140: Add support for null, true and false types - @thekid From 064505216e1bb30371520ddc88e953b9f5d0c933 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 12:26:36 +0100 Subject: [PATCH 623/926] Adapt to AST type refactoring See xp-framework/ast#39 --- composer.json | 2 +- .../php/lang/ast/emit/Declaration.class.php | 2 +- .../php/lang/ast/emit/GeneratedCode.class.php | 4 +- src/main/php/lang/ast/emit/PHP.class.php | 57 +++++++++++++------ .../php/lang/ast/emit/php/XpMeta.class.php | 6 +- .../unittest/emit/DeclarationTest.class.php | 3 +- .../unittest/emit/GeneratedCodeTest.class.php | 7 ++- .../emit/RewriteClassOnObjectsTest.class.php | 3 +- .../loader/CompilingClassLoaderTest.class.php | 2 +- 9 files changed, 56 insertions(+), 30 deletions(-) diff --git a/composer.json b/composer.json index d36e9284..9eb6e642 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": "^8.1", + "xp-framework/ast": "dev-refactor/types as 9.0.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index b8384f93..f3d58a3a 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -28,7 +28,7 @@ public function name() { return ltrim($this->type->name, '\\'); } public function rewriteEnumCase($member) { if (!self::$ENUMS && 'enum' === $this->type->kind) { return ($this->type->body[$member] ?? null) instanceof EnumCase; - } else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) { + } else if ('class' === $this->type->kind && $this->type->parent && '\\lang\\Enum' === $this->type->parent->literal()) { return ($this->type->body['$'.$member] ?? null) instanceof Property; } return false; diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 8fbe4d27..aff67379 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -70,11 +70,11 @@ public function lookup($type) { if ('self' === $type || 'static' === $type) { return new Declaration($this->type[0], $this); } else if ('parent' === $type) { - return $this->lookup($this->type[0]->parent); + return $this->lookup($this->type[0]->parent->literal()); } foreach ($this->type as $enclosing) { - if ($type === $enclosing->name) return new Declaration($enclosing, $this); + if ($enclosing->name && $type === $enclosing->name->literal()) return new Declaration($enclosing, $this); } return new Reflection($type); diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 949b4da9..9042989b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -14,7 +14,7 @@ UnpackExpression, Variable }; -use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable}; +use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsExpression}; use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { @@ -373,9 +373,17 @@ protected function emitEnum($result, $enum) { $enum->comment && $this->emitOne($result, $enum->comment); $enum->annotations && $this->emitOne($result, $enum->annotations); - $result->at($enum->declared)->out->write('enum '.$this->declaration($enum->name)); + $result->at($enum->declared)->out->write('enum '.$enum->declaration()); $enum->base && $result->out->write(':'.$enum->base); - $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); + + if ($enum->implements) { + $list= ''; + foreach ($enum->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); + } + $result->out->write('{'); foreach ($enum->body as $member) { @@ -386,7 +394,7 @@ protected function emitEnum($result, $enum) { $result->out->write('static function __init() {'); $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); - $result->out->write('}} '.$enum->name.'::__init();'); + $result->out->write('}} '.$enum->name->literal().'::__init();'); array_shift($result->type); } @@ -397,9 +405,17 @@ protected function emitClass($result, $class) { $class->comment && $this->emitOne($result, $class->comment); $class->annotations && $this->emitOne($result, $class->annotations); - $result->at($class->declared)->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $result->out->write(' extends '.$class->parent); - $class->implements && $result->out->write(' implements '.implode(', ', $class->implements)); + $result->at($class->declared)->out->write(implode(' ', $class->modifiers).' class '.$class->declaration()); + $class->parent && $result->out->write(' extends '.$class->parent->literal()); + + if ($class->implements) { + $list= ''; + foreach ($class->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); + } + $result->out->write('{'); foreach ($class->body as $member) { $this->emitOne($result, $member); @@ -444,7 +460,7 @@ protected function emitClass($result, $class) { $result->out->write('static function __init() {'); $this->emitInitializations($result, $result->locals[0]); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); - $result->out->write('}} '.$class->name.'::__init();'); + $result->out->write('}} '.$class->name->literal().'::__init();'); array_shift($result->type); $result->locals= []; } @@ -507,7 +523,7 @@ protected function emitInterface($result, $interface) { $interface->comment && $this->emitOne($result, $interface->comment); $interface->annotations && $this->emitOne($result, $interface->annotations); - $result->at($interface->declared)->out->write('interface '.$this->declaration($interface->name)); + $result->at($interface->declared)->out->write('interface '.$interface->declaration()); $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); $result->out->write('{'); foreach ($interface->body as $member) { @@ -523,7 +539,7 @@ protected function emitTrait($result, $trait) { $trait->comment && $this->emitOne($result, $trait->comment); $trait->annotations && $this->emitOne($result, $trait->annotations); - $result->at($trait->declared)->out->write('trait '.$this->declaration($trait->name)); + $result->at($trait->declared)->out->write('trait '.$trait->declaration()); $result->out->write('{'); foreach ($trait->body as $member) { $this->emitOne($result, $member); @@ -904,12 +920,12 @@ protected function emitArguments($result, $arguments) { } protected function emitNew($result, $new) { - if ($new->type instanceof Node) { + if ($new->type instanceof IsExpression) { $result->out->write('new ('); - $this->emitOne($result, $new->type); + $this->emitOne($result, $new->type->expression); $result->out->write(')('); } else { - $result->out->write('new '.$new->type.'('); + $result->out->write('new '.$new->type->literal().'('); } $this->emitArguments($result, $new->arguments); @@ -925,12 +941,19 @@ protected function emitNewClass($result, $new) { // Allow "extends self" to reference enclosing class (except if this // class is an anonymous class!) - if ('self' === $new->definition->parent && $result->type && $result->type[0]->name) { - $result->out->write(' extends '.$result->type[0]->name); + if ($result->type && $result->type[0]->name && $new->definition->parent && 'self' === $new->definition->parent->name()) { + $result->out->write(' extends '.$result->type[0]->name->literal()); } else if ($new->definition->parent) { - $result->out->write(' extends '.$new->definition->parent); + $result->out->write(' extends '.$new->definition->parent->literal()); + } + + if ($new->definition->implements) { + $list= ''; + foreach ($new->definition->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); } - $new->definition->implements && $result->out->write(' implements '.implode(', ', $new->definition->implements)); array_unshift($result->type, $new->definition); $result->out->write('{'); diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index fe947530..84c307f0 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -66,11 +66,11 @@ private function comment($comment) { } /** Emit xp::$meta */ - protected function emitMeta($result, $name, $annotations, $comment) { - if (null === $name) { + protected function emitMeta($result, $type, $annotations, $comment) { + if (null === $type) { $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); } else { - $result->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); + $result->out->write('\xp::$meta[\''.strtr($type->name(), '\\', '.').'\']= ['); } $result->out->write('"class" => ['); $this->attributes($result, $annotations, []); diff --git a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php index db3e751e..f5f2c3a1 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php @@ -2,6 +2,7 @@ use lang\ast\emit\Declaration; use lang\ast\nodes\{ClassDeclaration, Property}; +use lang\ast\types\IsValue; use unittest\{Assert, Test, Expect}; class DeclarationTest { @@ -9,7 +10,7 @@ class DeclarationTest { #[Before] public function type() { - $this->type= new ClassDeclaration([], '\\T', '\\lang\\Enum', [], [ + $this->type= new ClassDeclaration([], '\\T', new IsValue('\\lang\\Enum'), [], [ '$ONE' => new Property(['public', 'static'], 'ONE', null, null, [], null, 1) ]); } diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 3d1ed780..d77de87f 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -3,6 +3,7 @@ use io\streams\MemoryOutputStream; use lang\ast\emit\{GeneratedCode, Declaration, Escaping, Reflection}; use lang\ast\nodes\ClassDeclaration; +use lang\ast\types\IsValue; use lang\{Value, ClassNotFoundException}; use unittest\{Assert, Expect, Test}; @@ -46,7 +47,7 @@ public function writes_epilog_on_closing() { #[Test] public function lookup_self() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); } @@ -54,7 +55,7 @@ public function lookup_self() { #[Test] public function lookup_parent() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); + $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1); Assert::equals(new Reflection(Value::class), $r->lookup('parent')); } @@ -62,7 +63,7 @@ public function lookup_parent() { #[Test] public function lookup_named() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); } diff --git a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php index 7da479c8..7466106c 100755 --- a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php @@ -2,6 +2,7 @@ use lang\ast\emit\{PHP, RewriteClassOnObjects}; use lang\ast\nodes\{ScopeExpression, Variable, Literal, ClassDeclaration}; +use lang\ast\types\IsValue; use unittest\{Assert, Test}; class RewriteClassOnObjectsTest extends EmitterTraitTest { @@ -24,7 +25,7 @@ public function rewrites_type_variable() { public function does_not_rewrite_type_literal() { Assert::equals('self::class', $this->emit( new ScopeExpression('self', new Literal('class')), - [new ClassDeclaration([], '\\T', null, [], [], null, null, 1)] + [new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1)] )); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index caf47301..d8e6f925 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -131,7 +131,7 @@ public function load_uri() { Assert::equals('Tests', $class->getSimpleName()); } - #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "{", have "(end)"')] + #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "type name", have "(end)"')] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); From bc968cb931d2db23f7ac7374c0e6e8b575dc4b3c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 12:37:17 +0100 Subject: [PATCH 624/926] Fix ArbitrayNewExpressions trait (PHP 7.x compatibility) --- .../ast/emit/ArbitrayNewExpressions.class.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php index 34424999..6c332485 100755 --- a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php +++ b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php @@ -1,6 +1,7 @@ type instanceof Node) { + if (!($new->type instanceof IsExpression)) return parent::emitNew($result, $new); + + // Emit supported `new $var`, rewrite unsupported `new ($expr)` + if ($new->type->expression instanceof Variable) { + $result->out->write('new $'.$new->type->expression->name.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } else { $t= $result->temp(); $result->out->write('('.$t.'= '); - $this->emitOne($result, $new->type); + $this->emitOne($result, $new->type->expression); $result->out->write(') ? new '.$t.'('); $this->emitArguments($result, $new->arguments); $result->out->write(') : null'); - } else { - $result->out->write('new '.$new->type.'('); - $this->emitArguments($result, $new->arguments); - $result->out->write(')'); } } } \ No newline at end of file From 0fac8edd66ac07c6433a17aaab67c8cfeab227e8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 12:57:51 +0100 Subject: [PATCH 625/926] Rewrite implode() to foreach --- src/main/php/lang/ast/emit/RewriteEnums.class.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php index cb6c059a..0fd0edc8 100755 --- a/src/main/php/lang/ast/emit/RewriteEnums.class.php +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -16,8 +16,16 @@ protected function emitEnum($result, $enum) { array_unshift($result->meta, []); $result->locals= [[], []]; - $result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); - $enum->implements && $result->out->write(', '.implode(', ', $enum->implements)); + $result->out->write('final class '.$enum->declaration().' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); + + if ($enum->implements) { + $list= ''; + foreach ($enum->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write($list); + } + $result->out->write('{'); $cases= []; From 134e1363cd78542e35e605090bedfe2ff5776c44 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 22:46:07 +0100 Subject: [PATCH 626/926] Use xp-framework/ast release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9eb6e642..94e114f8 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": "dev-refactor/types as 9.0.0", + "xp-framework/ast": "^9.0", "php" : ">=7.0.0" }, "require-dev" : { From b12cf1c3213c11c5d2447bd8fffcac6e2bef175f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 22:48:54 +0100 Subject: [PATCH 627/926] Remove unused method declaration() --- src/main/php/lang/ast/emit/PHP.class.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9042989b..939e37c1 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -43,16 +43,6 @@ public function literal($type) { return null === $type ? null : $this->literals[get_class($type)]($type); } - /** - * Returns the simple name for use in a declaration - * - * @param string $name E.g. `\lang\ast\Parse` - * @return string In the above example, `Parse`. - */ - protected function declaration($name) { - return substr($name, strrpos($name, '\\') + 1); - } - /** * Returns whether a given node is a constant expression: * From 4add3e14a543160e1736bb14389df24d88554cdf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Nov 2022 23:46:59 +0100 Subject: [PATCH 628/926] Handle lang.types.IsGeneric --- src/main/php/lang/ast/emit/PHP70.class.php | 3 ++- src/main/php/lang/ast/emit/PHP71.class.php | 3 ++- src/main/php/lang/ast/emit/PHP72.class.php | 3 ++- src/main/php/lang/ast/emit/PHP74.class.php | 3 ++- src/main/php/lang/ast/emit/PHP80.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP81.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP82.class.php | 5 +++-- src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php | 3 ++- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 2a6cca27..ad92cc15 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -2,7 +2,7 @@ use lang\ast\Node; use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal}; -use lang\ast\types\{IsUnion, IsIntersection, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral}; +use lang\ast\types\{IsUnion, IsIntersection, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral, IsGeneric}; /** * PHP 7.0 syntax @@ -55,6 +55,7 @@ public function __construct() { $l= $t->literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, + IsGeneric::class => function($t) { return null; } ]; } diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 9b0f98a6..c5c7d314 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -1,6 +1,6 @@ literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, + IsGeneric::class => function($t) { return null; } ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 6e533036..070109dc 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -1,6 +1,6 @@ literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, + IsGeneric::class => function($t) { return null; } ]; } } \ 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 538d9f40..e54d5615 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -1,6 +1,6 @@ literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; }, + IsGeneric::class => function($t) { return null; } ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 37fdfc25..af43f354 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,7 +1,7 @@ literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - } + }, + IsGeneric::class => function($t) { return null; } ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index eba47ef5..607e1e68 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -1,7 +1,7 @@ literal(); return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - } + }, + IsGeneric::class => function($t) { return null; } ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 17dd3cb6..8a784cd9 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -1,7 +1,7 @@ function($t) { return $t->literal(); } + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } ]; } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index 37036a40..a8df4549 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -1,13 +1,14 @@ Date: Mon, 7 Nov 2022 00:01:05 +0100 Subject: [PATCH 629/926] Release 8.6.0 --- ChangeLog.md | 6 ++++++ composer.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 2a86de86..594837f3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.6.0 / 2022-11-06 + +* Bumped dependency on `xp-framework/ast` to version 9.1.0 - @thekid +* Merged PR #143: Adapt to AST type refactoring (xp-framework/ast#39) + (@thekid) + ## 8.5.1 / 2022-09-03 * Fixed issue #142: Nullable type unions don't work as expected - @thekid diff --git a/composer.json b/composer.json index 94e114f8..62ad447e 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": "^9.0", + "xp-framework/ast": "^9.1", "php" : ">=7.0.0" }, "require-dev" : { From 89ac2e7877caae23437334f2274f823536194a9f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 11:02:36 +0100 Subject: [PATCH 630/926] Add multiple destructuring tests Includes test for list() Reference Assignment, see #31 --- .../ast/unittest/emit/ArraysTest.class.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index fd5492e8..f2f6d848 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,6 +1,7 @@ =7.4.0")')] + public function reference_destructuring() { + $r= $this->run('class { + private $list= [1, 2]; + + public function run() { + [&$a, &$b]= $this->list; + $a++; + $b--; + return $this->list; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function list_destructuring() { + $r= $this->run('class { + public function run() { + list($a, $b)= [1, 2]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 2], $r); + } + #[Test] public function init_with_variable() { $r= $this->run('class { From edf73c413dc424e7c4824ba0423ec7470a8eb295 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 11:03:21 +0100 Subject: [PATCH 631/926] Run reference_destructuring() test in PHP 7.3 upwards --- src/test/php/lang/ast/unittest/emit/ArraysTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index f2f6d848..a6dfc554 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -52,7 +52,7 @@ public function run() { Assert::equals([1, 2], $r); } - #[Test, Action(eval: 'new RuntimeVersion(">=7.4.0")')] + #[Test, Action(eval: 'new RuntimeVersion(">=7.3.0")')] public function reference_destructuring() { $r= $this->run('class { private $list= [1, 2]; From 3e383802cd7928ab882b36f3ee8d7308ecf884db Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 11:12:45 +0100 Subject: [PATCH 632/926] Add more destructuring tests --- .../ast/unittest/emit/ArraysTest.class.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index a6dfc554..a7fe5ae3 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,7 +1,7 @@ run('class { + public function run() { + $a= 1; + $b= 2; + [$b, $a]= [$a, $b]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function result_of_destructuring() { + $r= $this->run('class { + public function run() { + return [$a, $b]= [1, 2]; + } + }'); + + Assert::equals([1, 2], $r); + } + #[Test] public function init_with_variable() { $r= $this->run('class { From 0ce74e89b12a77176a630a4433e9acad3a0dceaf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 12:02:53 +0100 Subject: [PATCH 633/926] Rewrite `list(&$a)= $expr` for PHP 7.0, 7.1 and 7.2 Implements #31 (w/o the foreach part) --- 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/PHP73.class.php | 56 ++++++++++++++++ .../ast/emit/RewriteDestructuring.class.php | 64 +++++++++++++++++++ .../ast/unittest/emit/ArraysTest.class.php | 15 +++-- 6 files changed, 131 insertions(+), 10 deletions(-) create mode 100755 src/main/php/lang/ast/emit/PHP73.class.php create mode 100755 src/main/php/lang/ast/emit/RewriteDestructuring.class.php diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index ad92cc15..bb306bec 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -21,12 +21,12 @@ class PHP70 extends PHP { OmitPropertyTypes, ReadonlyClasses, ReadonlyProperties, + RewriteDestructuring, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, RewriteMultiCatch, - RewriteNullCoalesceAssignment, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index c5c7d314..cd0300e3 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -21,10 +21,10 @@ class PHP71 extends PHP { ReadonlyProperties, ReadonlyClasses, RewriteClassOnObjects, + RewriteDestructuring, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, - RewriteNullCoalesceAssignment, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 070109dc..18e462a6 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -21,10 +21,10 @@ class PHP72 extends PHP { ReadonlyProperties, ReadonlyClasses, RewriteClassOnObjects, + RewriteDestructuring, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, - RewriteNullCoalesceAssignment, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php new file mode 100755 index 00000000..b0b70268 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -0,0 +1,56 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { + static $rewrite= [ + 'mixed' => 1, + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; + + $l= $t->literal(); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; + }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteDestructuring.class.php b/src/main/php/lang/ast/emit/RewriteDestructuring.class.php new file mode 100755 index 00000000..6d781cfb --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteDestructuring.class.php @@ -0,0 +1,64 @@ +operator) { + + // Inlined version of RewriteNullCoalesceAssignment + $this->emitAssign($result, $assignment->variable); + $result->out->write('??'); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); + $this->emitOne($result, $assignment->expression); + } else if ('array' === $assignment->variable->kind) { + + // Check whether the list assignment consists only of variables + $supported= true; + foreach ($assignment->variable->values as $pair) { + if ($pair[1] instanceof Variable) continue; + $supported= false; + break; + } + if ($supported) return parent::emitAssignment($result, $assignment); + + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + + // Create reference to right-hand if possible + $r= $assignment->expression; + if ( + ($r instanceof Variable) || + ($r instanceof InstanceExpression && $r->member instanceof Literal) || + ($r instanceof ScopeExpression && $r->member instanceof Variable) + ) { + $result->out->write('&'); + } + + $this->emitOne($result, $assignment->expression); + $result->out->write(')?null:['); + foreach ($assignment->variable->values as $i => $pair) { + + // Assign by reference + if ($pair[1] instanceof UnaryExpression) { + $this->emitAssign($result, $pair[1]->expression); + $result->out->write('='.$pair[1]->operator.$t.'['.$i.'],'); + } else { + $this->emitAssign($result, $pair[1]); + $result->out->write('='.$t.'['.$i.'],'); + } + } + $result->out->write(']'); + } else { + return parent::emitAssignment($result, $assignment); + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index a7fe5ae3..806fd843 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,7 +1,6 @@ =7.3.0")')] - public function reference_destructuring() { + #[Test, Values(['$list', '$this->instance', 'self::$static'])] + public function reference_destructuring($reference) { $r= $this->run('class { - private $list= [1, 2]; + private $instance= [1, 2]; + private static $static= [1, 2]; public function run() { - [&$a, &$b]= $this->list; + $list= [1, 2]; + [&$a, &$b]= '.$reference.'; $a++; $b--; - return $this->list; + return '.$reference.'; } }'); From 0752b352a3f04338c42e2ac9bd9a21bd94a7634e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 12:07:33 +0100 Subject: [PATCH 634/926] Inline RewriteNullCoalesceAssignment -> PHP 7.3 --- src/main/php/lang/ast/emit/PHP73.class.php | 17 ++++++++++++-- .../ast/emit/RewriteDestructuring.class.php | 4 +--- .../RewriteNullCoalesceAssignment.class.php | 22 ------------------- 3 files changed, 16 insertions(+), 27 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index b0b70268..d0e32976 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -3,8 +3,10 @@ use lang\ast\types\{IsUnion, IsIntersection, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral, IsGeneric}; /** - * PHP 7.3 syntax + * PHP 7.3 syntax. Same as PHP 7.2 but supports list reference assignments + * so only rewrites null-coalesce assignments. * + * @see https://wiki.php.net/rfc/list_reference_assignment * @see https://wiki.php.net/rfc#php_73 */ class PHP73 extends PHP { @@ -24,7 +26,6 @@ class PHP73 extends PHP { RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, - RewriteNullCoalesceAssignment, RewriteThrowableExpressions ; @@ -53,4 +54,16 @@ public function __construct() { IsGeneric::class => function($t) { return null; } ]; } + + protected function emitAssignment($result, $assignment) { + if ('??=' === $assignment->operator) { + $this->emitAssign($result, $assignment->variable); + $result->out->write('??'); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); + $this->emitOne($result, $assignment->expression); + } else { + parent::emitAssignment($result, $assignment); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteDestructuring.class.php b/src/main/php/lang/ast/emit/RewriteDestructuring.class.php index 6d781cfb..3f6109af 100755 --- a/src/main/php/lang/ast/emit/RewriteDestructuring.class.php +++ b/src/main/php/lang/ast/emit/RewriteDestructuring.class.php @@ -5,15 +5,13 @@ /** * Rewrites list reference assignments and null-coalesce for PHP <= 7.3 * - * @see lang.ast.emit.RewriteNullCoalesceAssignment + * @see https://wiki.php.net/rfc/null_coalesce_equal_operator * @see https://wiki.php.net/rfc/list_reference_assignment */ trait RewriteDestructuring { protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { - - // Inlined version of RewriteNullCoalesceAssignment $this->emitAssign($result, $assignment->variable); $result->out->write('??'); $this->emitOne($result, $assignment->variable); diff --git a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php deleted file mode 100755 index c86f30bd..00000000 --- a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php +++ /dev/null @@ -1,22 +0,0 @@ -operator) { - $this->emitAssign($result, $assignment->variable); - $result->out->write('??'); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->expression); - } else { - parent::emitAssignment($result, $assignment); - } - } -} \ No newline at end of file From 41db43bf0a6ff98f2b3152ba3ae666a16153690e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 12:19:09 +0100 Subject: [PATCH 635/926] Replace trait-based test for null-coalesce assignments with an emitter-based one --- .../emit/NullCoalesceAssignmentTest.class.php | 30 ++++++++++++++++++ ...ewriteNullCoalesceAssignmentTest.class.php | 31 ------------------- 2 files changed, 30 insertions(+), 31 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php new file mode 100755 index 00000000..9f664bc0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php @@ -0,0 +1,30 @@ +run('class { + public function run($arg) { + $arg??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r); + } + + #[Test, Values([[[], [true]], [[null], [true]]])] + public function fills_array_if_non_existant_or_null($value, $expected) { + $r= $this->run('class { + public function run($arg) { + $arg[0]??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php deleted file mode 100755 index 62f05a78..00000000 --- a/src/test/php/lang/ast/unittest/emit/RewriteNullCoalesceAssignmentTest.class.php +++ /dev/null @@ -1,31 +0,0 @@ -emit(new Assignment(new Variable('v'), '??=', new Literal('1'))) - ); - } - - #[Test] - public function does_not_rewrite_plus() { - Assert::equals( - '$v+=1', - $this->emit(new Assignment(new Variable('v'), '+=', new Literal('1'))) - ); - } -} \ No newline at end of file From 647da8112a4ab57b11189587d28ce38a51215c2a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 12:27:43 +0100 Subject: [PATCH 636/926] Rename RewriteDestructuring -> RewriteAssignments --- 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 +- ...riteDestructuring.class.php => RewriteAssignments.class.php} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/main/php/lang/ast/emit/{RewriteDestructuring.class.php => RewriteAssignments.class.php} (98%) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index bb306bec..417bf50f 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -21,7 +21,7 @@ class PHP70 extends PHP { OmitPropertyTypes, ReadonlyClasses, ReadonlyProperties, - RewriteDestructuring, + RewriteAssignments, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index cd0300e3..e8836a61 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -20,8 +20,8 @@ class PHP71 extends PHP { OmitPropertyTypes, ReadonlyProperties, ReadonlyClasses, + RewriteAssignments, RewriteClassOnObjects, - RewriteDestructuring, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 18e462a6..670bf3fb 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -20,8 +20,8 @@ class PHP72 extends PHP { OmitPropertyTypes, ReadonlyProperties, ReadonlyClasses, + RewriteAssignments, RewriteClassOnObjects, - RewriteDestructuring, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, diff --git a/src/main/php/lang/ast/emit/RewriteDestructuring.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php similarity index 98% rename from src/main/php/lang/ast/emit/RewriteDestructuring.class.php rename to src/main/php/lang/ast/emit/RewriteAssignments.class.php index 3f6109af..9b3e8ef1 100755 --- a/src/main/php/lang/ast/emit/RewriteDestructuring.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -8,7 +8,7 @@ * @see https://wiki.php.net/rfc/null_coalesce_equal_operator * @see https://wiki.php.net/rfc/list_reference_assignment */ -trait RewriteDestructuring { +trait RewriteAssignments { protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { From 25e261a3368b362d8277a72470ce1f15117e4186 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 12:30:48 +0100 Subject: [PATCH 637/926] Extend NullCoalesceAssignmentTest --- .../ast/unittest/emit/NullCoalesceAssignmentTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php index 9f664bc0..c8ab6c4d 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php @@ -16,7 +16,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[Test, Values([[[], [true]], [[null], [true]]])] + #[Test, Values([[[], true], [[null], true], [[false], false], [['Test'], 'Test']])] public function fills_array_if_non_existant_or_null($value, $expected) { $r= $this->run('class { public function run($arg) { @@ -25,6 +25,6 @@ public function run($arg) { } }', $value); - Assert::equals($expected, $r); + Assert::equals($expected, $r[0]); } } \ No newline at end of file From 0ff736a09c352f85ddd9bbf1612927cbd09d096d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 13:24:19 +0100 Subject: [PATCH 638/926] Assert destructuring returns non-array right-hand sides as-is --- .../php/lang/ast/unittest/emit/ArraysTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 806fd843..d5593f4e 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -106,6 +106,18 @@ public function run() { Assert::equals([1, 2], $r); } + #[Test, Values([null, true, false, 0, 0.5, '', 'Test'])] + public function destructuring_with_non_array($value) { + $r= $this->run('class { + public function run($arg) { + $r= [$a, $b]= $arg; + return [$a, $b, $r]; + } + }', $value); + + Assert::equals([null, null, $value], $r); + } + #[Test] public function init_with_variable() { $r= $this->run('class { From ad89ce18d2fbfce0549f653065f7c308204dfdf4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 13:31:28 +0100 Subject: [PATCH 639/926] Document list reference assignment support for PHP < 7.3 --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 594837f3..b32f8647 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #144: Rewrite `list(&$a)= $expr` for PHP 7.0, 7.1 and 7.2 + (@thekid) + ## 8.6.0 / 2022-11-06 * Bumped dependency on `xp-framework/ast` to version 9.1.0 - @thekid From 61d36c24af05a89c268862aa0704961e2444ee79 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 13:48:27 +0100 Subject: [PATCH 640/926] Add support for specifying keys in list() See https://wiki.php.net/rfc/list_keys --- src/main/php/lang/ast/emit/PHP.class.php | 4 + src/main/php/lang/ast/emit/PHP70.class.php | 27 ++++++- .../ast/emit/RewriteAssignments.class.php | 80 +++++++++++-------- .../ast/unittest/emit/ArraysTest.class.php | 12 +++ 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 939e37c1..de0d959b 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -697,6 +697,10 @@ protected function emitAssign($result, $target) { } else if ('array' === $target->kind) { $result->out->write('list('); foreach ($target->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } $this->emitAssign($result, $pair[1]); $result->out->write(','); } diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 417bf50f..b55919f5 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,7 +1,7 @@ out->write(')?function(...'.$a.') use('.$t.') { return '.$t.'(...'.$a.'); }:'); $result->out->write('(function() { throw new \Error("Given argument is not callable"); })())'); } + + protected function emitAssignment($result, $assignment) { + if ('??=' === $assignment->operator) { + + // Rewrite null-coalesce operator + $this->emitAssign($result, $assignment->variable); + $result->out->write('??'); + $this->emitOne($result, $assignment->variable); + $result->out->write('='); + $this->emitOne($result, $assignment->expression); + return; + } else if ('array' === $assignment->variable->kind) { + + // Rewrite destructuring unless assignment consists only of variables + $r= false; + foreach ($assignment->variable->values as $pair) { + if (!$pair[0] && $pair[1] instanceof Variable) continue; + $r= true; + break; + } + if ($r) return $this->rewriteDestructuring($result, $assignment); + } + + return parent::emitAssignment($result, $assignment); + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index 9b3e8ef1..19439e14 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -10,53 +10,65 @@ */ trait RewriteAssignments { + protected function rewriteDestructuring($result, $assignment) { + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + + // Create reference to right-hand if possible + $r= $assignment->expression; + if ( + ($r instanceof Variable) || + ($r instanceof InstanceExpression && $r->member instanceof Literal) || + ($r instanceof ScopeExpression && $r->member instanceof Variable) + ) { + $result->out->write('&'); + } + + $this->emitOne($result, $assignment->expression); + $result->out->write(')?null:['); + foreach ($assignment->variable->values as $i => $pair) { + + // Assign by reference + if ($pair[1] instanceof UnaryExpression) { + $this->emitAssign($result, $pair[1]->expression); + $result->out->write('='.$pair[1]->operator.$t.'['); + } else { + $this->emitAssign($result, $pair[1]); + $result->out->write('='.$t.'['); + } + + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('],'); + } else { + $result->out->write($i.'],'); + } + } + $result->out->write(']'); + } + protected function emitAssignment($result, $assignment) { if ('??=' === $assignment->operator) { + + // Rewrite null-coalesce operator $this->emitAssign($result, $assignment->variable); $result->out->write('??'); $this->emitOne($result, $assignment->variable); $result->out->write('='); $this->emitOne($result, $assignment->expression); + return; } else if ('array' === $assignment->variable->kind) { - // Check whether the list assignment consists only of variables - $supported= true; + // Rewrite destructuring unless assignment consists only of variables + $r= false; foreach ($assignment->variable->values as $pair) { if ($pair[1] instanceof Variable) continue; - $supported= false; + $r= true; break; } - if ($supported) return parent::emitAssignment($result, $assignment); - - $t= $result->temp(); - $result->out->write('null===('.$t.'='); - - // Create reference to right-hand if possible - $r= $assignment->expression; - if ( - ($r instanceof Variable) || - ($r instanceof InstanceExpression && $r->member instanceof Literal) || - ($r instanceof ScopeExpression && $r->member instanceof Variable) - ) { - $result->out->write('&'); - } - - $this->emitOne($result, $assignment->expression); - $result->out->write(')?null:['); - foreach ($assignment->variable->values as $i => $pair) { - - // Assign by reference - if ($pair[1] instanceof UnaryExpression) { - $this->emitAssign($result, $pair[1]->expression); - $result->out->write('='.$pair[1]->operator.$t.'['.$i.'],'); - } else { - $this->emitAssign($result, $pair[1]); - $result->out->write('='.$t.'['.$i.'],'); - } - } - $result->out->write(']'); - } else { - return parent::emitAssignment($result, $assignment); + if ($r) return $this->rewriteDestructuring($result, $assignment); } + + return parent::emitAssignment($result, $assignment); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index d5593f4e..d9156dd4 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -51,6 +51,18 @@ public function run() { Assert::equals([1, 2], $r); } + #[Test] + public function destructuring_map() { + $r= $this->run('class { + public function run() { + ["two" => $a, "one" => $b]= ["one" => 1, "two" => 2]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + #[Test, Values(['$list', '$this->instance', 'self::$static'])] public function reference_destructuring($reference) { $r= $this->run('class { From fb43b1ecb6ff23ac18089b6ed1a8e47310a3e594 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 13:51:54 +0100 Subject: [PATCH 641/926] Verify keys can be an expression See https://wiki.php.net/rfc/list_keys#should_arbitrary_expressions_be_allowed_as_keys --- .../lang/ast/unittest/emit/ArraysTest.class.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index d9156dd4..2c496740 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -63,6 +63,20 @@ public function run() { Assert::equals([2, 1], $r); } + #[Test] + public function destructuring_map_with_expression() { + $r= $this->run('class { + private $one= "one"; + public function run() { + $two= "two"; + [$two => $a, $this->one => $b]= ["one" => 1, "two" => 2]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + #[Test, Values(['$list', '$this->instance', 'self::$static'])] public function reference_destructuring($reference) { $r= $this->run('class { From 5e6b76226811f7a1ed320ea642ba289704b97d26 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 13:56:06 +0100 Subject: [PATCH 642/926] Document support for specifying keys in list() --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b32f8647..3ebc8738 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #145: Add support for specifying keys in list(), which has + been in PHP since 7.1.0, see https://wiki.php.net/rfc/list_keys + (@thekid) * Merged PR #144: Rewrite `list(&$a)= $expr` for PHP 7.0, 7.1 and 7.2 (@thekid) From f75dc57c7645e20f2f9885ac7d4d48ee6f9e5192 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 14:46:08 +0100 Subject: [PATCH 643/926] Add tests for calling properties and methods "list" --- .../ast/unittest/emit/MembersTest.class.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index beca43f1..e952095e 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -99,6 +99,32 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function list_property() { + $r= $this->run('class { + private $list= [1, 2, 3]; + + public function run() { + return $this->list; + } + }'); + + Assert::equals([1, 2, 3], $r); + } + + #[Test] + public function list_method() { + $r= $this->run('class { + private function list() { return [1, 2, 3]; } + + public function run() { + return $this->list(); + } + }'); + + Assert::equals([1, 2, 3], $r); + } + #[Test, Values(['variable', 'invocation', 'array'])] public function class_on_objects($via) { $t= $this->type('class { From 63a204cc865b8ec41a020679ccb5b83474864b0f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 15:03:39 +0100 Subject: [PATCH 644/926] Add support for omitting expressions in destructuring assignments E.g. `list($a, , $b)= $expr` or `[, $a]= $expr`. --- composer.json | 2 +- src/main/php/lang/ast/Emitter.class.php | 11 ++++++ .../ast/emit/ArrayUnpackUsingMerge.class.php | 4 ++- src/main/php/lang/ast/emit/PHP.class.php | 6 ++-- src/main/php/lang/ast/emit/PHP70.class.php | 2 +- .../ast/emit/RewriteAssignments.class.php | 2 +- .../ast/unittest/emit/ArraysTest.class.php | 34 +++++++++++++++++++ 7 files changed, 55 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 62ad447e..5c312305 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": "^9.1", + "xp-framework/ast": "^9.2", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 148dd1e9..912e488a 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -44,6 +44,17 @@ public static function forRuntime($runtime, $emitters= []) { throw new IllegalArgumentException('XP Compiler does not support '.$runtime.' yet'); } + /** + * Raises an exception + * + * @param string $error + * @param ?Throwable $cause + * @return never + */ + public function raise($error, $cause= null) { + throw new IllegalStateException($error, $cause); + } + /** * Transforms nodes of a certain kind using the given function, which * may return either single node, which will be then emitted, or an diff --git a/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php index d527b63f..4d90205a 100755 --- a/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php +++ b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php @@ -19,7 +19,8 @@ protected function emitArray($result, $array) { $unpack= false; foreach ($array->values as $pair) { - if ('unpack' === $pair[1]->kind) { + $element= $pair[1] ?? $this->raise('Cannot use empty array elements in arrays'); + if ('unpack' === $element->kind) { $unpack= true; break; } @@ -32,6 +33,7 @@ protected function emitArray($result, $array) { $this->emitOne($result, $pair[0]); $result->out->write('=>'); } + if ('unpack' === $pair[1]->kind) { if ('array' === $pair[1]->expression->kind) { $result->out->write('],'); diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index de0d959b..92320dee 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -265,7 +265,7 @@ protected function emitArray($result, $array) { $this->emitOne($result, $pair[0]); $result->out->write('=>'); } - $this->emitOne($result, $pair[1]); + $this->emitOne($result, $pair[1] ?? $this->raise('Cannot use empty array elements in arrays')); $result->out->write(','); } $result->out->write(']'); @@ -701,7 +701,9 @@ protected function emitAssign($result, $target) { $this->emitOne($result, $pair[0]); $result->out->write('=>'); } - $this->emitAssign($result, $pair[1]); + if ($pair[1]) { + $this->emitAssign($result, $pair[1]); + } $result->out->write(','); } $result->out->write(')'); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index b55919f5..5d59f666 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -121,7 +121,7 @@ protected function emitAssignment($result, $assignment) { // Rewrite destructuring unless assignment consists only of variables $r= false; foreach ($assignment->variable->values as $pair) { - if (!$pair[0] && $pair[1] instanceof Variable) continue; + if (null === $pair[0] && (null === $pair[1] || $pair[1] instanceof Variable)) continue; $r= true; break; } diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index 19439e14..6b9210b8 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -62,7 +62,7 @@ protected function emitAssignment($result, $assignment) { // Rewrite destructuring unless assignment consists only of variables $r= false; foreach ($assignment->variable->values as $pair) { - if ($pair[1] instanceof Variable) continue; + if (null === $pair[1] || $pair[1] instanceof Variable) continue; $r= true; break; } diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 2c496740..8a71e07d 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,5 +1,6 @@ run('class { + public function run() { + return '.$input.'; + } + }'); + } + #[Test] public function destructuring() { $r= $this->run('class { @@ -51,6 +61,30 @@ public function run() { Assert::equals([1, 2], $r); } + #[Test] + public function destructuring_with_empty_between() { + $r= $this->run('class { + public function run() { + [$a, , $b]= [1, 2, 3]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 3], $r); + } + + #[Test] + public function destructuring_with_empty_start() { + $r= $this->run('class { + public function run() { + [, $a, $b]= [1, 2, 3]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 3], $r); + } + #[Test] public function destructuring_map() { $r= $this->run('class { From acfb53cedd3e59fb56c7e78a2b499392fd888a0c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 15:15:46 +0100 Subject: [PATCH 645/926] Correctly handle non-array case --- .../ast/emit/RewriteAssignments.class.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index 6b9210b8..f7a939f7 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -12,7 +12,7 @@ trait RewriteAssignments { protected function rewriteDestructuring($result, $assignment) { $t= $result->temp(); - $result->out->write('null===('.$t.'='); + $result->out->write('is_array('.$t.'='); // Create reference to right-hand if possible $r= $assignment->expression; @@ -25,8 +25,12 @@ protected function rewriteDestructuring($result, $assignment) { } $this->emitOne($result, $assignment->expression); - $result->out->write(')?null:['); + $result->out->write(')?['); foreach ($assignment->variable->values as $i => $pair) { + if (null === $pair[1]) { + $result->out->write('null,'); + continue; + } // Assign by reference if ($pair[1] instanceof UnaryExpression) { @@ -44,7 +48,17 @@ protected function rewriteDestructuring($result, $assignment) { $result->out->write($i.'],'); } } - $result->out->write(']'); + $result->out->write(']:(['); + foreach ($assignment->variable->values as $pair) { + if ($pair[1] instanceof UnaryExpression) { + $this->emitAssign($result, $pair[1]->expression); + $result->out->write('=null,'); + } else if ($pair[1]) { + $this->emitAssign($result, $pair[1]); + $result->out->write('=null,'); + } + } + $result->out->write(']?'.$t.':null)'); } protected function emitAssignment($result, $assignment) { From 48e9735c79019e71c3102791f4caa8a3b41f9bbb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 15:20:48 +0100 Subject: [PATCH 646/926] Document support for omitting expressions in list assignments --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3ebc8738..582dfb8c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #146: Add support for omitting expressions in destructuring + assignments, e.g. `list($a, , $b)= $expr` or `[, $a]= $expr`. + (@thekid) * Merged PR #145: Add support for specifying keys in list(), which has been in PHP since 7.1.0, see https://wiki.php.net/rfc/list_keys (@thekid) From 550d57d5b2e8c439d493001b24e3d7f612076992 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 16:35:47 +0100 Subject: [PATCH 647/926] Add test for nested destructuring --- .../php/lang/ast/unittest/emit/ArraysTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 8a71e07d..8222c744 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -61,6 +61,18 @@ public function run() { Assert::equals([1, 2], $r); } + #[Test] + public function nested_destructuring() { + $r= $this->run('class { + public function run() { + [[$a, $b], $c]= [[1, 2], 3]; + return [$a, $b, $c]; + } + }'); + + Assert::equals([1, 2, 3], $r); + } + #[Test] public function destructuring_with_empty_between() { $r= $this->run('class { From f74a23270215fd517a6bdf878cfb09d286c3ea7f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 16:53:37 +0100 Subject: [PATCH 648/926] Fix nested destructuring assignments with keys for PHP 7.0 --- .../ast/emit/RewriteAssignments.class.php | 37 ++++++++++--------- .../ast/unittest/emit/ArraysTest.class.php | 12 ++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index f7a939f7..99c6397e 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -1,6 +1,14 @@ out->write('&'); } + $temp= new Variable(substr($t, 1)); $this->emitOne($result, $assignment->expression); $result->out->write(')?['); foreach ($assignment->variable->values as $i => $pair) { @@ -33,30 +42,24 @@ protected function rewriteDestructuring($result, $assignment) { } // Assign by reference + $value= new OffsetExpression($temp, $pair[0] ?? new Literal($i)); if ($pair[1] instanceof UnaryExpression) { - $this->emitAssign($result, $pair[1]->expression); - $result->out->write('='.$pair[1]->operator.$t.'['); + $this->emitAssignment($result, new Assignment($pair[1]->expression, '=&', $value)); } else { - $this->emitAssign($result, $pair[1]); - $result->out->write('='.$t.'['); - } - - if ($pair[0]) { - $this->emitOne($result, $pair[0]); - $result->out->write('],'); - } else { - $result->out->write($i.'],'); + $this->emitAssignment($result, new Assignment($pair[1], '=', $value)); } + $result->out->write(','); } + + $null= new Literal('null'); $result->out->write(']:(['); foreach ($assignment->variable->values as $pair) { if ($pair[1] instanceof UnaryExpression) { - $this->emitAssign($result, $pair[1]->expression); - $result->out->write('=null,'); - } else if ($pair[1]) { - $this->emitAssign($result, $pair[1]); - $result->out->write('=null,'); + $this->emitAssignment($result, new Assignment($pair[1]->expression, '=', $null)); + } else { + $this->emitAssignment($result, new Assignment($pair[1], '=', $null)); } + $result->out->write(','); } $result->out->write(']?'.$t.':null)'); } diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 8222c744..91ef7c6f 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -123,6 +123,18 @@ public function run() { Assert::equals([2, 1], $r); } + #[Test] + public function nested_destructuring_with_map() { + $r= $this->run('class { + public function run() { + ["nested" => ["one" => $a], "three" => $b]= ["nested" => ["one" => 1], "three" => 3]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 3], $r); + } + #[Test, Values(['$list', '$this->instance', 'self::$static'])] public function reference_destructuring($reference) { $r= $this->run('class { From 718eaade0e368193d50ad651ef525a5623316c5a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 17:10:32 +0100 Subject: [PATCH 649/926] Check for empty expressions --- src/main/php/lang/ast/emit/RewriteAssignments.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index 99c6397e..bdfa536d 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -56,7 +56,7 @@ protected function rewriteDestructuring($result, $assignment) { foreach ($assignment->variable->values as $pair) { if ($pair[1] instanceof UnaryExpression) { $this->emitAssignment($result, new Assignment($pair[1]->expression, '=', $null)); - } else { + } else if ($pair[1]) { $this->emitAssignment($result, new Assignment($pair[1], '=', $null)); } $result->out->write(','); From aba6668f7a01069567ac8d016fa6f94f855d8bb3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:06:41 +0100 Subject: [PATCH 650/926] Fix foreach with destructuring assignments in PHP 7.0 --- ChangeLog.md | 2 ++ src/main/php/lang/ast/emit/PHP70.class.php | 26 +++++++++++++++++++ .../ast/unittest/emit/LoopsTest.class.php | 26 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 582dfb8c..949fa27b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed `foreach` statement using destructuring assignments in PHP 7.0 + (@thekid) * Merged PR #146: Add support for omitting expressions in destructuring assignments, e.g. `list($a, , $b)= $expr` or `[, $a]= $expr`. (@thekid) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 5d59f666..c635a921 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -130,4 +130,30 @@ protected function emitAssignment($result, $assignment) { return parent::emitAssignment($result, $assignment); } + + protected function emitForeach($result, $foreach) { + $result->out->write('foreach ('); + $this->emitOne($result, $foreach->expression); + $result->out->write(' as '); + if ($foreach->key) { + $this->emitOne($result, $foreach->key); + $result->out->write(' => '); + } + + // Short list syntax didn't arrive until PHP 7.1 + if ('array' === $foreach->value->kind) { + $result->out->write('list('); + foreach ($foreach->value->values as $pair) { + $pair[1] && $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(')'); + } else { + $this->emitOne($result, $foreach->value); + } + + $result->out->write(') {'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index fbfd05ae..cf1ad973 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -47,6 +47,32 @@ public function run() { Assert::equals('1,2,3', $r); } + #[Test] + public function foreach_with_destructuring() { + $r= $this->run('class { + public function run() { + $result= ""; + foreach ([[1, 2], [3, 4]] as [$a, $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 2,3 & 4', $r); + } + + #[Test] + public function foreach_with_destructuring_and_missing_expressions() { + $r= $this->run('class { + public function run() { + $result= ""; + foreach ([[1, 2, 3], [4, 5, 6]] as [$a, , $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 3,4 & 6', $r); + } + #[Test] public function for_loop() { $r= $this->run('class { From aa1c3346660c8d205bf9c07ba2e65117536e1bbe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:22:23 +0100 Subject: [PATCH 651/926] Support destructuring with keys as part of `foreach` in PHP 7.0 --- src/main/php/lang/ast/emit/PHP.class.php | 16 ++++++++++++- src/main/php/lang/ast/emit/PHP70.class.php | 23 +++++++++---------- .../ast/emit/RewriteAssignments.class.php | 4 +++- .../ast/unittest/emit/LoopsTest.class.php | 13 +++++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 92320dee..f36183a7 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -829,7 +829,21 @@ protected function emitForeach($result, $foreach) { $this->emitOne($result, $foreach->key); $result->out->write(' => '); } - $this->emitOne($result, $foreach->value); + + if ('array' === $foreach->value->kind) { + $result->out->write('['); + foreach ($foreach->value->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $pair[1] && $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(']'); + } else { + $this->emitOne($result, $foreach->value); + } $result->out->write(') {'); $this->emitAll($result, $foreach->body); $result->out->write('}'); diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index c635a921..f2c63807 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,7 +1,7 @@ out->write(' => '); } - // Short list syntax didn't arrive until PHP 7.1 + // Rewrite destructuring assignment as first statement in loop if ('array' === $foreach->value->kind) { - $result->out->write('list('); - foreach ($foreach->value->values as $pair) { - $pair[1] && $this->emitOne($result, $pair[1]); - $result->out->write(','); - } - $result->out->write(')'); + $t= $result->temp(); + $result->out->write($t.') {'); + $this->rewriteDestructuring($result, new Assignment($foreach->value, '=', new Variable(substr($t, 1)))); + $result->out->write(';'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); } else { $this->emitOne($result, $foreach->value); + $result->out->write(') {'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); } - - $result->out->write(') {'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index bdfa536d..e6236e35 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -54,7 +54,9 @@ protected function rewriteDestructuring($result, $assignment) { $null= new Literal('null'); $result->out->write(']:(['); foreach ($assignment->variable->values as $pair) { - if ($pair[1] instanceof UnaryExpression) { + if (null === $pair[1]) { + continue; + } else if ($pair[1] instanceof UnaryExpression) { $this->emitAssignment($result, new Assignment($pair[1]->expression, '=', $null)); } else if ($pair[1]) { $this->emitAssignment($result, new Assignment($pair[1], '=', $null)); diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index cf1ad973..7d7f2558 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -73,6 +73,19 @@ public function run() { Assert::equals('1 & 3,4 & 6', $r); } + #[Test] + public function foreach_with_destructuring_keys() { + $r= $this->run('class { + public function run() { + $result= ""; + foreach ([["a" => 1, "b" => 2], ["a" => 3, "b" => 4]] as ["a" => $a, "b" => $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 2,3 & 4', $r); + } + #[Test] public function for_loop() { $r= $this->run('class { From 12cbf1b4768d2a0cc2cf9e2fa2717662e1c44284 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:34:40 +0100 Subject: [PATCH 652/926] Rewrite destructuring assignments in foreach to first statement Adds support for `list()` reference assignments in PHP < 7.3 & fixes #31 --- ChangeLog.md | 2 + .../ForeachDestructuringAsStatement.class.php | 41 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP70.class.php | 28 +------------ src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + .../ast/unittest/emit/LoopsTest.class.php | 17 ++++++++ 6 files changed, 64 insertions(+), 26 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 949fa27b..758bf4fc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed issue #31: Support `list()` reference assignment for PHP < 7.3 + (@thekid) * Fixed `foreach` statement using destructuring assignments in PHP 7.0 (@thekid) * Merged PR #146: Add support for omitting expressions in destructuring diff --git a/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php b/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php new file mode 100755 index 00000000..f49e46db --- /dev/null +++ b/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php @@ -0,0 +1,41 @@ +out->write('foreach ('); + $this->emitOne($result, $foreach->expression); + $result->out->write(' as '); + if ($foreach->key) { + $this->emitOne($result, $foreach->key); + $result->out->write(' => '); + } + + if ('array' === $foreach->value->kind) { + $t= $result->temp(); + $result->out->write('&'.$t.') {'); + $this->rewriteDestructuring($result, new Assignment($foreach->value, '=', new Variable(substr($t, 1)))); + $result->out->write(';'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); + } else { + $this->emitOne($result, $foreach->value); + $result->out->write(') {'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index f2c63807..9c39301f 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,7 +1,7 @@ out->write('foreach ('); - $this->emitOne($result, $foreach->expression); - $result->out->write(' as '); - if ($foreach->key) { - $this->emitOne($result, $foreach->key); - $result->out->write(' => '); - } - - // Rewrite destructuring assignment as first statement in loop - if ('array' === $foreach->value->kind) { - $t= $result->temp(); - $result->out->write($t.') {'); - $this->rewriteDestructuring($result, new Assignment($foreach->value, '=', new Variable(substr($t, 1)))); - $result->out->write(';'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); - } else { - $this->emitOne($result, $foreach->value); - $result->out->write(') {'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); - } - } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index e8836a61..1c416f31 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -13,6 +13,7 @@ class PHP71 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 670bf3fb..769faff8 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -13,6 +13,7 @@ class PHP72 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index 7d7f2558..ff8d74fa 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -86,6 +86,23 @@ public function run() { Assert::equals('1 & 2,3 & 4', $r); } + #[Test] + public function foreach_with_destructuring_references() { + $r= $this->run('class { + private $list= [[1, 2], [3, 4]]; + + public function run() { + foreach ($this->list as [&$a, &$b]) { + $a*= 3; + $b*= 2; + } + return $this->list; + } + }'); + + Assert::equals([[3, 4], [9, 8]], $r); + } + #[Test] public function for_loop() { $r= $this->run('class { From d5eab1c9bd74319b610a4392d7d7b3615c811fc2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:36:20 +0100 Subject: [PATCH 653/926] Simplify code in emitAssignment() --- src/main/php/lang/ast/emit/PHP70.class.php | 5 +---- src/main/php/lang/ast/emit/RewriteAssignments.class.php | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 9c39301f..14009e53 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -120,13 +120,10 @@ protected function emitAssignment($result, $assignment) { } else if ('array' === $assignment->variable->kind) { // Rewrite destructuring unless assignment consists only of variables - $r= false; foreach ($assignment->variable->values as $pair) { if (null === $pair[0] && (null === $pair[1] || $pair[1] instanceof Variable)) continue; - $r= true; - break; + return $this->rewriteDestructuring($result, $assignment); } - if ($r) return $this->rewriteDestructuring($result, $assignment); } return parent::emitAssignment($result, $assignment); diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php index e6236e35..634e19b4 100755 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ b/src/main/php/lang/ast/emit/RewriteAssignments.class.php @@ -79,13 +79,10 @@ protected function emitAssignment($result, $assignment) { } else if ('array' === $assignment->variable->kind) { // Rewrite destructuring unless assignment consists only of variables - $r= false; foreach ($assignment->variable->values as $pair) { if (null === $pair[1] || $pair[1] instanceof Variable) continue; - $r= true; - break; + return $this->rewriteDestructuring($result, $assignment); } - if ($r) return $this->rewriteDestructuring($result, $assignment); } return parent::emitAssignment($result, $assignment); From 6f910cc4616d4fdde969a25264407b200c764965 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:47:14 +0100 Subject: [PATCH 654/926] QA: WS --- .../php/lang/ast/emit/ForeachDestructuringAsStatement.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php b/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php index f49e46db..46c24523 100755 --- a/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php +++ b/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php @@ -4,7 +4,7 @@ /** * Rewrite destructuring assignment as first statement in `foreach` loops. - * + * * This ensures compatibility with: * * - PHP 7.0, which neither supports keys nor references From e83ade4bb66be67b4e649a51b0b334776c02b66a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 19:47:33 +0100 Subject: [PATCH 655/926] Release 8.7.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 758bf4fc..3e578b62 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.7.0 / 2022-11-12 + * Fixed issue #31: Support `list()` reference assignment for PHP < 7.3 (@thekid) * Fixed `foreach` statement using destructuring assignments in PHP 7.0 From 2046193205113d84d28e707e6338eba329ef9455 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 12 Nov 2022 20:06:14 +0100 Subject: [PATCH 656/926] Fix usage --- src/main/php/xp/compiler/Usage.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 874ac47f..b2208f14 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -27,7 +27,7 @@ public function add($t, $active= false) { } } - $language= Language::named(self::RUNTIME); + $language= Language::named(strtoupper(self::RUNTIME)); foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { if ($class->isSubclassOf(Language::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { $impl->add($class, $class->isInstance($language)); From bfd059785ea3412ed2ded70dcd76463c5bec5421 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 23 Nov 2022 21:02:18 +0100 Subject: [PATCH 657/926] Prevent NPE when looking up parent in a class without parent --- src/main/php/lang/ast/emit/GeneratedCode.class.php | 2 +- .../lang/ast/unittest/emit/GeneratedCodeTest.class.php | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index aff67379..67004aa6 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -70,7 +70,7 @@ public function lookup($type) { if ('self' === $type || 'static' === $type) { return new Declaration($this->type[0], $this); } else if ('parent' === $type) { - return $this->lookup($this->type[0]->parent->literal()); + return $this->type[0]->parent ? $this->lookup($this->type[0]->parent->literal()) : null; } foreach ($this->type as $enclosing) { diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index d77de87f..2234a449 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -60,6 +60,14 @@ public function lookup_parent() { Assert::equals(new Reflection(Value::class), $r->lookup('parent')); } + #[Test] + public function lookup_parent_without_parent() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); + + Assert::null($r->lookup('parent')); + } + #[Test] public function lookup_named() { $r= new GeneratedCode(new MemoryOutputStream()); From d6bd2ad8bfbcb7689d046c2e984c982889a16d3a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 28 Nov 2022 20:27:19 +0100 Subject: [PATCH 658/926] Add test for enums with constants See https://wiki.php.net/rfc/constants_in_traits --- src/test/php/lang/ast/unittest/emit/TraitsTest.class.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php index 141c950a..d619093c 100755 --- a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php @@ -1,7 +1,8 @@ getMethod('loaded')->invoke($instance)); Assert::equals('Not spinning', $t->getMethod('noLongerSpinning')->invoke($instance)); } + + #[Test, Action(eval: 'new RuntimeVersion(">=8.2.0-dev")')] + public function can_have_constants() { + $t= $this->type('trait { const FIXTURE = 1; }'); + Assert::equals(1, $t->getConstant('FIXTURE')); + } } \ No newline at end of file From 6258126e834b40765ef1710caa63611f849432c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Dec 2022 12:12:00 +0100 Subject: [PATCH 659/926] Run tests with PHP 8.3 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa3fb1bd..0f3ac5a1 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions == '8.2' }} + continue-on-error: ${{ matrix.php-versions == '8.3' }} strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] os: [ubuntu-latest, windows-latest] steps: From 56d4bec0ff3b05922432e3cc551afbf40fe5e4b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Dec 2022 14:57:13 +0100 Subject: [PATCH 660/926] Make enclosing type(s) accessible via code generator --- src/main/php/lang/ast/CodeGen.class.php | 10 +++ .../php/lang/ast/emit/GeneratedCode.class.php | 13 +-- src/main/php/lang/ast/emit/InType.class.php | 15 ++++ src/main/php/lang/ast/emit/PHP.class.php | 84 ++++++++----------- .../lang/ast/emit/ReadonlyClasses.class.php | 3 +- .../ast/emit/ReadonlyProperties.class.php | 5 +- src/main/php/lang/ast/emit/Result.class.php | 1 - .../php/lang/ast/emit/RewriteEnums.class.php | 9 +- .../emit/php/VirtualPropertyTypes.class.php | 8 +- .../php/lang/ast/emit/php/XpMeta.class.php | 2 +- .../unittest/emit/EmitterTraitTest.class.php | 6 +- .../unittest/emit/GeneratedCodeTest.class.php | 14 ++-- 12 files changed, 91 insertions(+), 79 deletions(-) create mode 100755 src/main/php/lang/ast/emit/InType.class.php diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index 755e85f9..7495b2a9 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -2,10 +2,20 @@ class CodeGen { private $id= 0; + public $context= []; /** Creates a new, unique symbol */ public function symbol() { return '_'.($this->id++); } + public function enter($context) { + array_unshift($this->context, $context); + return $context; + } + + public function leave() { + return array_shift($this->context); + } + /** * Search a given scope recursively for nodes with a given kind * diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 67004aa6..4b76a4d0 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -3,7 +3,6 @@ class GeneratedCode extends Result { private $prolog, $epilog; public $line= 1; - public $type= []; /** * Starts a result stream, including an optional prolog and epilog @@ -67,14 +66,18 @@ public function temp() { * @return lang.ast.emit.Type */ public function lookup($type) { + $enclosing= $this->codegen->context[0] ?? null; + if ('self' === $type || 'static' === $type) { - return new Declaration($this->type[0], $this); + return new Declaration($enclosing->type, $this); } else if ('parent' === $type) { - return $this->type[0]->parent ? $this->lookup($this->type[0]->parent->literal()) : null; + return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null; } - foreach ($this->type as $enclosing) { - if ($enclosing->name && $type === $enclosing->name->literal()) return new Declaration($enclosing, $this); + foreach ($this->codegen->context as $context) { + if ($context->type->name && $type === $context->type->name->literal()) { + return new Declaration($context->type, $this); + } } return new Reflection($type); diff --git a/src/main/php/lang/ast/emit/InType.class.php b/src/main/php/lang/ast/emit/InType.class.php new file mode 100755 index 00000000..ae1a7de0 --- /dev/null +++ b/src/main/php/lang/ast/emit/InType.class.php @@ -0,0 +1,15 @@ +type= $type; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f36183a7..04044c49 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -287,7 +287,7 @@ protected function emitParameter($result, $parameter) { $this->emitOne($result, $parameter->default); } else { $result->out->write('=null'); - $result->locals[1]['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; + $result->codegen->context[0]->init[InType::INSTANCE]['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; } } $result->locals[$parameter->name]= true; @@ -357,9 +357,7 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { - array_unshift($result->type, $enum); - array_unshift($result->meta, []); - $result->locals= [[], []]; + $context= $result->codegen->enter(new InType($enum)); $enum->comment && $this->emitOne($result, $enum->comment); $enum->annotations && $this->emitOne($result, $enum->annotations); @@ -375,23 +373,19 @@ protected function emitEnum($result, $enum) { } $result->out->write('{'); - foreach ($enum->body as $member) { $this->emitOne($result, $member); } - - // Initializations $result->out->write('static function __init() {'); - $this->emitInitializations($result, $result->locals[0]); + $this->emitInitializations($result, $context->init[InType::STATICS]); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); $result->out->write('}} '.$enum->name->literal().'::__init();'); - array_shift($result->type); + + $result->codegen->leave(); } protected function emitClass($result, $class) { - array_unshift($result->type, $class); - array_unshift($result->meta, []); - $result->locals ?: $result->locals= [[], [], []]; + $context= $result->codegen->context[0] ?? $result->codegen->enter(new InType($class)); $class->comment && $this->emitOne($result, $class->comment); $class->annotations && $this->emitOne($result, $class->annotations); @@ -412,26 +406,26 @@ protected function emitClass($result, $class) { } // Virtual properties support: __virtual member + __get() and __set() - if ($result->locals[2]) { + if ($context->virtual) { $result->out->write('private $__virtual= ['); - foreach ($result->locals[2] as $name => $access) { + foreach ($context->virtual as $name => $access) { $name && $result->out->write("'{$name}' => null,"); } $result->out->write('];'); $result->out->write('public function __get($name) { switch ($name) {'); - foreach ($result->locals[2] as $name => $access) { + foreach ($context->virtual as $name => $access) { $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[0]); $result->out->write('break;'); } - isset($result->locals[2][null]) || $result->out->write( + isset($context->virtual[null]) || $result->out->write( 'default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING);' ); $result->out->write('}}'); $result->out->write('public function __set($name, $value) { switch ($name) {'); - foreach ($result->locals[2] as $name => $access) { + foreach ($context->virtual as $name => $access) { $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[1]); $result->out->write('break;'); @@ -441,18 +435,18 @@ protected function emitClass($result, $class) { // Create constructor for property initializations to non-static scalars // which have not already been emitted inside constructor - if ($result->locals[1]) { + if ($context->init[InType::INSTANCE]) { $result->out->write('public function __construct() {'); - $this->emitInitializations($result, $result->locals[1]); + $this->emitInitializations($result, $context->init[InType::INSTANCE]); $result->out->write('}'); } $result->out->write('static function __init() {'); - $this->emitInitializations($result, $result->locals[0]); + $this->emitInitializations($result, $context->init[InType::STATICS]); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name->literal().'::__init();'); - array_shift($result->type); - $result->locals= []; + + $result->codegen->leave(); } protected function emitMeta($result, $name, $annotations, $comment) { @@ -509,12 +503,13 @@ protected function emitAnnotations($result, $annotations) { } protected function emitInterface($result, $interface) { - array_unshift($result->meta, []); + $result->codegen->enter(new InType($interface)); $interface->comment && $this->emitOne($result, $interface->comment); $interface->annotations && $this->emitOne($result, $interface->annotations); $result->at($interface->declared)->out->write('interface '.$interface->declaration()); $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); + $result->out->write('{'); foreach ($interface->body as $member) { $this->emitOne($result, $member); @@ -522,10 +517,11 @@ protected function emitInterface($result, $interface) { $result->out->write('}'); $this->emitMeta($result, $interface->name, $interface->annotations, $interface->comment); + $result->codegen->leave(); } protected function emitTrait($result, $trait) { - array_unshift($result->meta, []); + $result->codegen->enter(new InType($trait)); $trait->comment && $this->emitOne($result, $trait->comment); $trait->annotations && $this->emitOne($result, $trait->annotations); @@ -537,6 +533,7 @@ protected function emitTrait($result, $trait) { $result->out->write('}'); $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); + $result->codegen->leave(); } protected function emitUse($result, $use) { @@ -561,7 +558,7 @@ protected function emitConst($result, $const) { } protected function emitProperty($result, $property) { - $result->meta[0][self::PROPERTY][$property->name]= [ + $result->codegen->context[0]->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, @@ -577,27 +574,17 @@ protected function emitProperty($result, $property) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { - $result->locals[0]['self::$'.$property->name]= $property->expression; + $result->codegen->context[0]->init[InType::STATICS]['self::$'.$property->name]= $property->expression; } else { - $result->locals[1]['$this->'.$property->name]= $property->expression; + $result->codegen->context[0]->init[InType::INSTANCE]['$this->'.$property->name]= $property->expression; } } $result->out->write(';'); } protected function emitMethod($result, $method) { - - // Include non-static initializations for members in constructor, removing - // them to signal emitClass() no constructor needs to be generated. - if ('__construct' === $method->name && isset($result->locals[1])) { - $locals= [[], $result->locals[1], [], 'this' => true]; - $result->locals[1]= []; - } else { - $locals= [[], [], [], 'this' => true]; - } $result->stack[]= $result->locals; - $result->locals= $locals; - + $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', DETAIL_ANNOTATIONS => $method->annotations, @@ -628,7 +615,8 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'); - $this->emitInitializations($result, $result->locals[1]); + $this->emitInitializations($result, $result->codegen->context[0]->init[InType::INSTANCE]); + $result->codegen->context[0]->init[InType::INSTANCE]= []; foreach ($promoted as $param) { $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } @@ -640,13 +628,8 @@ protected function emitMethod($result, $method) { $this->emitProperty($result, new Property(explode(' ', $param->promote), $param->name, $param->type)); } - // Copy any virtual properties inside locals[2] to class scope - $virtual= $result->locals[2]; $result->locals= array_pop($result->stack); - foreach ($virtual as $name => $access) { - $result->locals[2][$name]= $access; - } - $result->meta[0][self::METHOD][$method->name]= $meta; + $result->codegen->context[0]->meta[self::METHOD][$method->name]= $meta; } protected function emitBraced($result, $braced) { @@ -943,7 +926,7 @@ protected function emitNew($result, $new) { } protected function emitNewClass($result, $new) { - array_unshift($result->meta, []); + $enclosing= $result->codegen->context[0] ?? null; $result->out->write('(new class('); $this->emitArguments($result, $new->arguments); @@ -951,8 +934,8 @@ protected function emitNewClass($result, $new) { // Allow "extends self" to reference enclosing class (except if this // class is an anonymous class!) - if ($result->type && $result->type[0]->name && $new->definition->parent && 'self' === $new->definition->parent->name()) { - $result->out->write(' extends '.$result->type[0]->name->literal()); + if ($enclosing && $enclosing->type->name && $new->definition->parent && 'self' === $new->definition->parent->name()) { + $result->out->write(' extends '.$enclosing->type->name->literal()); } else if ($new->definition->parent) { $result->out->write(' extends '.$new->definition->parent->literal()); } @@ -965,7 +948,7 @@ protected function emitNewClass($result, $new) { $result->out->write(' implements '.substr($list, 2)); } - array_unshift($result->type, $new->definition); + $result->codegen->enter(new InType($new->definition)); $result->out->write('{'); foreach ($new->definition->body as $member) { $this->emitOne($result, $member); @@ -973,8 +956,7 @@ protected function emitNewClass($result, $new) { $result->out->write('function __new() {'); $this->emitMeta($result, null, [], null); $result->out->write('return $this; }})->__new()'); - - array_shift($result->type); + $result->codegen->leave(); } protected function emitCallable($result, $callable) { diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index ab29f97f..715c9b84 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -28,7 +28,8 @@ protected function emitClass($result, $class) { // Prevent dynamic members $throw= new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);'); - $result->locals= [[], [], [null => [$throw, $throw]]]; + $context= $result->codegen->enter(new InType($class)); + $context->virtual[null]= [$throw, $throw]; } return parent::emitClass($result, $class); diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 9ebfa550..d877c7de 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -23,11 +23,12 @@ protected function emitProperty($result, $property) { if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property); + $context= $result->codegen->context[0]; $modifiers= 0; foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } - $result->meta[0][self::PROPERTY][$property->name]= [ + $context->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, @@ -53,7 +54,7 @@ protected function emitProperty($result, $property) { } // Create virtual property implementing the readonly semantics - $result->locals[2][$property->name]= [ + $context->virtual[$property->name]= [ new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $property->name)), new Code(sprintf( ($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'). diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php index a9b7dd58..bd034524 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -7,7 +7,6 @@ class Result implements Closeable { public $out; public $codegen; - public $meta= []; public $locals= []; public $stack= []; diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php index 0fd0edc8..01d74483 100755 --- a/src/main/php/lang/ast/emit/RewriteEnums.class.php +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -12,10 +12,7 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { - array_unshift($result->type, $enum); - array_unshift($result->meta, []); - $result->locals= [[], []]; - + $context= $result->codegen->enter(new InType($enum)); $result->out->write('final class '.$enum->declaration().' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); if ($enum->implements) { @@ -82,9 +79,9 @@ protected function emitEnum($result, $enum) { $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); } } - $this->emitInitializations($result, $result->locals[0]); + $this->emitInitializations($result, $context->init[InType::STATICS]); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); $result->out->write('}} '.$enum->name.'::__init();'); - array_shift($result->type); + $result->codegen->leave(); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index d202e69b..a61843c4 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -1,6 +1,7 @@ locals[2][$property->name]= [ + $context= $result->codegen->context[0]; + $context->virtual[$property->name]= [ new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), new Code(sprintf( $check.$assign. @@ -78,7 +80,7 @@ protected function emitProperty($result, $property) { // Initialize via constructor if (isset($property->expression)) { - $result->locals[1]['$this->'.$property->name]= $property->expression; + $context->init[InType::INSTANCE]['$this->'.$property->name]= $property->expression; } // Emit XP meta information for the reflection API @@ -86,7 +88,7 @@ protected function emitProperty($result, $property) { foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } - $result->meta[0][self::PROPERTY][$property->name]= [ + $context->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index 84c307f0..e29a594e 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -76,7 +76,7 @@ protected function emitMeta($result, $type, $annotations, $comment) { $this->attributes($result, $annotations, []); $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); - foreach (array_shift($result->meta) as $type => $lookup) { + foreach ($result->codegen->context[0]->meta as $type => $lookup) { $result->out->write($type.' => ['); foreach ($lookup as $key => $meta) { $result->out->write("'".$key."' => ["); diff --git a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php index 620eed0a..f4dd307f 100755 --- a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php @@ -2,7 +2,7 @@ use io\streams\MemoryOutputStream; use lang\ast\Node; -use lang\ast\emit\GeneratedCode; +use lang\ast\emit\{InType, GeneratedCode}; use unittest\Before; abstract class EmitterTraitTest { @@ -11,7 +11,9 @@ abstract class EmitterTraitTest { /** Emits a node and returns the emitted code */ protected function emit(Node $node, array $type= []): string { $result= new GeneratedCode(new MemoryOutputStream(), ''); - $result->type= $type; + foreach ($type as $t) { + $result->codegen->enter(new InType($t)); + } $this->emitter->emitOne($result, $node); return $result->out->bytes(); diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 2234a449..b13584dd 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -1,7 +1,7 @@ type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); + $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); + Assert::equals(new Declaration($context->type, $r), $r->lookup('self')); } #[Test] public function lookup_parent() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1); + $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1))); Assert::equals(new Reflection(Value::class), $r->lookup('parent')); } @@ -63,7 +63,7 @@ public function lookup_parent() { #[Test] public function lookup_parent_without_parent() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); + $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); Assert::null($r->lookup('parent')); } @@ -71,9 +71,9 @@ public function lookup_parent_without_parent() { #[Test] public function lookup_named() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->type[0]= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); + $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); + Assert::equals(new Declaration($context->type, $r), $r->lookup('\\T')); } #[Test] From 97423d6b925f027c7e39378d473c68a3e2836d5f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 3 Dec 2022 19:42:27 +0100 Subject: [PATCH 661/926] Simplify instance and static member inits --- src/main/php/lang/ast/emit/InType.class.php | 6 ++---- src/main/php/lang/ast/emit/PHP.class.php | 18 +++++++++--------- .../php/lang/ast/emit/RewriteEnums.class.php | 2 +- .../emit/php/VirtualPropertyTypes.class.php | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/php/lang/ast/emit/InType.class.php b/src/main/php/lang/ast/emit/InType.class.php index ae1a7de0..4df91107 100755 --- a/src/main/php/lang/ast/emit/InType.class.php +++ b/src/main/php/lang/ast/emit/InType.class.php @@ -1,12 +1,10 @@ emitOne($result, $parameter->default); } else { $result->out->write('=null'); - $result->codegen->context[0]->init[InType::INSTANCE]['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; + $result->codegen->context[0]->init['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; } } $result->locals[$parameter->name]= true; @@ -377,7 +377,7 @@ protected function emitEnum($result, $enum) { $this->emitOne($result, $member); } $result->out->write('static function __init() {'); - $this->emitInitializations($result, $context->init[InType::STATICS]); + $this->emitInitializations($result, $context->statics); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); $result->out->write('}} '.$enum->name->literal().'::__init();'); @@ -435,14 +435,14 @@ protected function emitClass($result, $class) { // Create constructor for property initializations to non-static scalars // which have not already been emitted inside constructor - if ($context->init[InType::INSTANCE]) { + if ($context->init) { $result->out->write('public function __construct() {'); - $this->emitInitializations($result, $context->init[InType::INSTANCE]); + $this->emitInitializations($result, $context->init); $result->out->write('}'); } $result->out->write('static function __init() {'); - $this->emitInitializations($result, $context->init[InType::STATICS]); + $this->emitInitializations($result, $context->statics); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); $result->out->write('}} '.$class->name->literal().'::__init();'); @@ -574,9 +574,9 @@ protected function emitProperty($result, $property) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { - $result->codegen->context[0]->init[InType::STATICS]['self::$'.$property->name]= $property->expression; + $result->codegen->context[0]->statics['self::$'.$property->name]= $property->expression; } else { - $result->codegen->context[0]->init[InType::INSTANCE]['$this->'.$property->name]= $property->expression; + $result->codegen->context[0]->init['$this->'.$property->name]= $property->expression; } } $result->out->write(';'); @@ -615,8 +615,8 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'); - $this->emitInitializations($result, $result->codegen->context[0]->init[InType::INSTANCE]); - $result->codegen->context[0]->init[InType::INSTANCE]= []; + $this->emitInitializations($result, $result->codegen->context[0]->init); + $result->codegen->context[0]->init= []; foreach ($promoted as $param) { $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php index 01d74483..8cd863df 100755 --- a/src/main/php/lang/ast/emit/RewriteEnums.class.php +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -79,7 +79,7 @@ protected function emitEnum($result, $enum) { $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); } } - $this->emitInitializations($result, $context->init[InType::STATICS]); + $this->emitInitializations($result, $context->statics); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); $result->out->write('}} '.$enum->name.'::__init();'); $result->codegen->leave(); diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index a61843c4..7960e2d9 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -80,7 +80,7 @@ protected function emitProperty($result, $property) { // Initialize via constructor if (isset($property->expression)) { - $context->init[InType::INSTANCE]['$this->'.$property->name]= $property->expression; + $context->init['$this->'.$property->name]= $property->expression; } // Emit XP meta information for the reflection API From 92621206c4bd4e3db907c60c2b1a5c2b94228357 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Dec 2022 11:10:37 +0100 Subject: [PATCH 662/926] Rename context -> scope --- src/main/php/lang/ast/CodeGen.class.php | 10 +++++----- .../php/lang/ast/emit/GeneratedCode.class.php | 8 ++++---- src/main/php/lang/ast/emit/PHP.class.php | 18 +++++++++--------- .../lang/ast/emit/ReadonlyProperties.class.php | 6 +++--- .../emit/php/VirtualPropertyTypes.class.php | 8 ++++---- .../php/lang/ast/emit/php/XpMeta.class.php | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index 7495b2a9..439a1957 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -2,18 +2,18 @@ class CodeGen { private $id= 0; - public $context= []; + public $scope= []; /** Creates a new, unique symbol */ public function symbol() { return '_'.($this->id++); } - public function enter($context) { - array_unshift($this->context, $context); - return $context; + public function enter($scope) { + array_unshift($this->scope, $scope); + return $scope; } public function leave() { - return array_shift($this->context); + return array_shift($this->scope); } /** diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 4b76a4d0..558c0a42 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -66,7 +66,7 @@ public function temp() { * @return lang.ast.emit.Type */ public function lookup($type) { - $enclosing= $this->codegen->context[0] ?? null; + $enclosing= $this->codegen->scope[0] ?? null; if ('self' === $type || 'static' === $type) { return new Declaration($enclosing->type, $this); @@ -74,9 +74,9 @@ public function lookup($type) { return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null; } - foreach ($this->codegen->context as $context) { - if ($context->type->name && $type === $context->type->name->literal()) { - return new Declaration($context->type, $this); + foreach ($this->codegen->scope as $scope) { + if ($scope->type->name && $type === $scope->type->name->literal()) { + return new Declaration($scope->type, $this); } } diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 762894b0..34201b42 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -287,7 +287,7 @@ protected function emitParameter($result, $parameter) { $this->emitOne($result, $parameter->default); } else { $result->out->write('=null'); - $result->codegen->context[0]->init['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; + $result->codegen->scope[0]->init['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; } } $result->locals[$parameter->name]= true; @@ -385,7 +385,7 @@ protected function emitEnum($result, $enum) { } protected function emitClass($result, $class) { - $context= $result->codegen->context[0] ?? $result->codegen->enter(new InType($class)); + $context= $result->codegen->scope[0] ?? $result->codegen->enter(new InType($class)); $class->comment && $this->emitOne($result, $class->comment); $class->annotations && $this->emitOne($result, $class->annotations); @@ -558,7 +558,7 @@ protected function emitConst($result, $const) { } protected function emitProperty($result, $property) { - $result->codegen->context[0]->meta[self::PROPERTY][$property->name]= [ + $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, @@ -574,9 +574,9 @@ protected function emitProperty($result, $property) { $result->out->write('='); $this->emitOne($result, $property->expression); } else if (in_array('static', $property->modifiers)) { - $result->codegen->context[0]->statics['self::$'.$property->name]= $property->expression; + $result->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression; } else { - $result->codegen->context[0]->init['$this->'.$property->name]= $property->expression; + $result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression; } } $result->out->write(';'); @@ -615,8 +615,8 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'); - $this->emitInitializations($result, $result->codegen->context[0]->init); - $result->codegen->context[0]->init= []; + $this->emitInitializations($result, $result->codegen->scope[0]->init); + $result->codegen->scope[0]->init= []; foreach ($promoted as $param) { $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } @@ -629,7 +629,7 @@ protected function emitMethod($result, $method) { } $result->locals= array_pop($result->stack); - $result->codegen->context[0]->meta[self::METHOD][$method->name]= $meta; + $result->codegen->scope[0]->meta[self::METHOD][$method->name]= $meta; } protected function emitBraced($result, $braced) { @@ -926,7 +926,7 @@ protected function emitNew($result, $new) { } protected function emitNewClass($result, $new) { - $enclosing= $result->codegen->context[0] ?? null; + $enclosing= $result->codegen->scope[0] ?? null; $result->out->write('(new class('); $this->emitArguments($result, $new->arguments); diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index d877c7de..5dcbee40 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -23,12 +23,12 @@ protected function emitProperty($result, $property) { if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property); - $context= $result->codegen->context[0]; + $scope= $result->codegen->scope[0]; $modifiers= 0; foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } - $context->meta[self::PROPERTY][$property->name]= [ + $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, @@ -54,7 +54,7 @@ protected function emitProperty($result, $property) { } // Create virtual property implementing the readonly semantics - $context->virtual[$property->name]= [ + $scope->virtual[$property->name]= [ new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $property->name)), new Code(sprintf( ($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'). diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php index 7960e2d9..65a64e02 100755 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php @@ -67,8 +67,8 @@ protected function emitProperty($result, $property) { $check= ''; } - $context= $result->codegen->context[0]; - $context->virtual[$property->name]= [ + $scope= $result->codegen->scope[0]; + $scope->virtual[$property->name]= [ new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), new Code(sprintf( $check.$assign. @@ -80,7 +80,7 @@ protected function emitProperty($result, $property) { // Initialize via constructor if (isset($property->expression)) { - $context->init['$this->'.$property->name]= $property->expression; + $scope->init['$this->'.$property->name]= $property->expression; } // Emit XP meta information for the reflection API @@ -88,7 +88,7 @@ protected function emitProperty($result, $property) { foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } - $context->meta[self::PROPERTY][$property->name]= [ + $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, DETAIL_COMMENT => $property->comment, diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index e29a594e..0da5bcc8 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -76,7 +76,7 @@ protected function emitMeta($result, $type, $annotations, $comment) { $this->attributes($result, $annotations, []); $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); - foreach ($result->codegen->context[0]->meta as $type => $lookup) { + foreach ($result->codegen->scope[0]->meta as $type => $lookup) { $result->out->write($type.' => ['); foreach ($lookup as $key => $meta) { $result->out->write("'".$key."' => ["); From 2540b6d2463c556a70a7fe9228189fa008dfc9db Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Dec 2022 11:13:11 +0100 Subject: [PATCH 663/926] Remove additional check on type --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 34201b42..1a94db1d 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -934,7 +934,7 @@ protected function emitNewClass($result, $new) { // Allow "extends self" to reference enclosing class (except if this // class is an anonymous class!) - if ($enclosing && $enclosing->type->name && $new->definition->parent && 'self' === $new->definition->parent->name()) { + if ($enclosing && $new->definition->parent && 'self' === $new->definition->parent->name()) { $result->out->write(' extends '.$enclosing->type->name->literal()); } else if ($new->definition->parent) { $result->out->write(' extends '.$new->definition->parent->literal()); From 9fd13b366d32fc22370273a015d5056eee63fc43 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Dec 2022 11:29:56 +0100 Subject: [PATCH 664/926] Release 8.8.0 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3e578b62..00f40e40 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.0 / 2022-12-04 + +* Merged PR #152: Make enclosing type(s) accessible via code generator + (@thekid) + ## 8.7.0 / 2022-11-12 * Fixed issue #31: Support `list()` reference assignment for PHP < 7.3 From 899510d31a54fe17d4f07c7861c249cca40b0066 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 Dec 2022 22:31:53 +0100 Subject: [PATCH 665/926] Fix casing of lang.ast.CompilingClassLoader in imports --- src/main/php/xp/compiler/CompileRunner.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 4858ad20..b776a8fb 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -1,7 +1,7 @@ Date: Fri, 30 Dec 2022 17:43:07 +0100 Subject: [PATCH 666/926] Add missing import for Expect annotation --- src/test/php/lang/ast/unittest/emit/ArraysTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 91ef7c6f..c0d4a0b3 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,7 +1,7 @@ Date: Fri, 30 Dec 2022 17:53:40 +0100 Subject: [PATCH 667/926] Rewrite map to list --- src/test/php/lang/ast/unittest/emit/BracesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index db77a66f..ce2f4fb0 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -82,7 +82,7 @@ public function run() { Assert::equals(320, $r); } - #[Test, Values(['map' => ['(__LINE__)."test"' => '3test', '(__LINE__) + 1' => 4, '(__LINE__) - 1' => 2,]])] + #[Test, Values([['(__LINE__)."test"', '3test'], ['(__LINE__) + 1', 4], ['(__LINE__) - 1', 2]])] public function global_constant_in_braces_not_confused_with_cast($input, $expected) { $r= $this->run('class { public function run() { From c0771d8d9fdeaf72a8c3f7efc30b1946be1189ab Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 17:56:17 +0100 Subject: [PATCH 668/926] Add missing import for Before annotation --- src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php index f5f2c3a1..2c674f47 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php @@ -3,7 +3,7 @@ use lang\ast\emit\Declaration; use lang\ast\nodes\{ClassDeclaration, Property}; use lang\ast\types\IsValue; -use unittest\{Assert, Test, Expect}; +use unittest\{Assert, Before, Test, Expect}; class DeclarationTest { private $type; From 6e176ea827c657f668d2f3f038c51b9b991d490b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 17:56:53 +0100 Subject: [PATCH 669/926] Add missing import for Expect annotation --- src/test/php/lang/ast/unittest/emit/DeclareTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php index 47ae6802..860ebd35 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -1,7 +1,7 @@ Date: Fri, 30 Dec 2022 17:58:10 +0100 Subject: [PATCH 670/926] Add missing import for Ignore, Expect and Values annotations --- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 7c15a682..28b93368 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -3,7 +3,7 @@ use lang\reflect\TargetInvocationException; use lang\{Enum, Error}; use unittest\actions\VerifyThat; -use unittest\{Assert, Action, Test}; +use unittest\{Assert, Action, Ignore, Expect, Values, Test}; #[Action(eval: 'new VerifyThat(fn() => function_exists("enum_exists"))')] class EnumTest extends EmittingTest { From 7c25b4fc2a4e61af860b6e8d550c014091386235 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 19:27:36 +0100 Subject: [PATCH 671/926] Add missing import for Values annotation --- src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index b13584dd..164ba788 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -5,7 +5,7 @@ use lang\ast\nodes\ClassDeclaration; use lang\ast\types\IsValue; use lang\{Value, ClassNotFoundException}; -use unittest\{Assert, Expect, Test}; +use unittest\{Assert, Expect, Values, Test}; class GeneratedCodeTest { From c840e79865362c3c0e5755dd30b41304b70af096 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 19:27:47 +0100 Subject: [PATCH 672/926] Add missing import for Expect annotation --- src/test/php/lang/ast/unittest/emit/LambdasTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 92e1999b..d118bb24 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -3,7 +3,7 @@ use lang\ast\Errors; use lang\ast\nodes\{ClosureExpression, LambdaExpression}; use unittest\actions\VerifyThat; -use unittest\{Action, Assert, Test}; +use unittest\{Action, Assert, Expect, Test}; /** * Lambdas (a.k.a. arrow functions) support From 68cd6f8568a8da3e490c5630869da4c8d3bbd128 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 19:30:08 +0100 Subject: [PATCH 673/926] Add missing import for Ignore and and Values annotations --- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index f13ae5a5..2ac312dc 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -1,7 +1,7 @@ Date: Fri, 30 Dec 2022 19:42:55 +0100 Subject: [PATCH 674/926] Rewrite map to list --- src/test/php/lang/ast/unittest/emit/TernaryTest.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 40f68ad2..7bffb2a7 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -5,7 +5,7 @@ class TernaryTest extends EmittingTest { - #[Test, Values(map: [true => 'OK', false => 'Fail'])] + #[Test, Values([[true, 'OK'], [false, 'Fail']])] public function ternary($value, $result) { Assert::equals($result, $this->run( 'class { @@ -17,7 +17,7 @@ public function run($value) { )); } - #[Test, Values(map: [true => MODIFIER_PUBLIC, false => MODIFIER_PRIVATE])] + #[Test, Values([[true, MODIFIER_PUBLIC], [false, MODIFIER_PRIVATE]])] public function ternary_constants_goto_label_ambiguity($value, $result) { Assert::equals($result, $this->run( 'class { @@ -29,7 +29,7 @@ public function run($value) { )); } - #[Test, Values(['map' => ['OK' => 'OK', null => 'Fail']])] + #[Test, Values([['OK', 'OK'], [null, 'Fail']])] public function short_ternary($value, $result) { Assert::equals($result, $this->run( 'class { From d944f0b5bc53aae9cb592eba97e4898b351bad72 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 19:52:42 +0100 Subject: [PATCH 675/926] Close files before trying to erase directory --- src/test/php/lang/ast/unittest/cli/FromFileTest.class.php | 1 + src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php index a358696b..b0b73eb6 100755 --- a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php @@ -21,6 +21,7 @@ public function folder() { #[After] public function cleanup() { + $this->file->isOpen() && $this->file->close(); $this->folder->unlink(); } diff --git a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php index c0d0d1a8..6b2067ef 100755 --- a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php @@ -21,6 +21,7 @@ public function folder() { #[After] public function cleanup() { + $this->archive->isOpen() && $this->archive->close(); $this->folder->unlink(); } From 32ecdc790b2b6034ee53ea4881b4b1a587438687 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 30 Dec 2022 19:54:53 +0100 Subject: [PATCH 676/926] Add missing import for Values annotations --- src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php | 2 +- src/test/php/lang/ast/unittest/cli/InputTest.class.php | 2 +- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index a8df4549..267c4205 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -2,7 +2,7 @@ use lang\ast\emit\{PHP70, PHP71, PHP72, PHP74, PHP80, PHP81, PHP82}; use lang\ast\types\{IsLiteral, IsArray, IsFunction, IsMap, IsValue, IsNullable, IsUnion, IsIntersection, IsGeneric}; -use unittest\{Assert, Test}; +use unittest\{Assert, Test, Values}; class TypeLiteralsTest { diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php index b59d0d74..dc58270e 100755 --- a/src/test/php/lang/ast/unittest/cli/InputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -2,7 +2,7 @@ use io\{File, Folder}; use lang\{Environment, IllegalArgumentException}; -use unittest\{Assert, After, Before, Test, Values}; +use unittest\{Assert, After, Before, Expect, Test, Values}; use util\cmd\Console; use xp\compiler\{Input, FromStream, FromFile, FromFilesIn, FromInputs}; diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index d8e6f925..abe44609 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -4,7 +4,7 @@ use lang\ast\CompilingClassLoader; use lang\{ClassFormatException, ClassNotFoundException, ElementNotFoundException, ClassLoader, Environment}; use unittest\actions\RuntimeVersion; -use unittest\{Action, Assert, Expect, Test, TestCase}; +use unittest\{Action, Assert, Expect, Test, Values}; class CompilingClassLoaderTest { private static $runtime; From cddc9b0d030f74988e021b1d365a01b22a9e63ac Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Jan 2023 15:35:45 +0100 Subject: [PATCH 677/926] Resolve ambiguity when exactly one unnamed argument is present See https://github.com/xp-framework/reflection/pull/27 --- src/main/php/lang/ast/emit/php/XpMeta.class.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index 0da5bcc8..4c445551 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -9,6 +9,7 @@ * Code compiled with this optimization in place requires using XP Core as * a dependency! * + * @see https://github.com/xp-framework/reflection/pull/27 * @see https://github.com/xp-framework/rfc/issues/336 */ trait XpMeta { @@ -29,6 +30,7 @@ protected function annotations($result, $annotations) { } else if (1 === sizeof($arguments) && isset($arguments[0])) { $this->emitOne($result, $arguments[0]); $result->out->write(','); + $lookup[$name]= 1; // Resolve ambiguity } else { $result->out->write('['); foreach ($arguments as $name => $argument) { From e2afe50f8b62146fb9bbcac407890a388377cd5f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 29 Jan 2023 15:37:49 +0100 Subject: [PATCH 678/926] Remove PHP 8.3 for the moment, it fails on `windows-latest` --- .github/workflows/ci.yml | 2 +- ChangeLog.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f3ac5a1..ceca1e9d 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] os: [ubuntu-latest, windows-latest] steps: diff --git a/ChangeLog.md b/ChangeLog.md index 00f40e40..0c9314cd 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.1 / 2023-01-29 + +* Added flag to meta information to resolve ambiguity when exactly one + unnamed annotation argument is present and an array or NULL. See + xp-framework/reflection#27 + (@thekid) + ## 8.8.0 / 2022-12-04 * Merged PR #152: Make enclosing type(s) accessible via code generator From 29921155345be033f2aaa0c8d87a349cfd30851d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 3 Feb 2023 22:20:06 +0100 Subject: [PATCH 679/926] Migrate to new testing library --- .github/workflows/ci.yml | 6 +++--- composer.json | 2 +- .../lang/ast/unittest/EmitterTest.class.php | 8 ++++---- .../lang/ast/unittest/ResultTest.class.php | 6 +++--- .../ast/unittest/TypeLiteralsTest.class.php | 18 ++++++++--------- .../unittest/cli/CompileOnlyTest.class.php | 2 +- .../ast/unittest/cli/FromFileTest.class.php | 2 +- .../unittest/cli/FromFilesInTest.class.php | 4 ++-- .../ast/unittest/cli/FromInputsTest.class.php | 6 +++--- .../lang/ast/unittest/cli/InputTest.class.php | 4 ++-- .../ast/unittest/cli/OutputTest.class.php | 4 ++-- .../ast/unittest/cli/ToArchiveTest.class.php | 2 +- .../ast/unittest/cli/ToFileTest.class.php | 2 +- .../ast/unittest/cli/ToFolderTest.class.php | 4 ++-- .../ast/unittest/cli/ToStreamTest.class.php | 2 +- .../unittest/emit/AnnotationSupport.class.php | 2 +- .../emit/AnonymousClassTest.class.php | 2 +- .../emit/ArgumentPromotionTest.class.php | 4 ++-- .../emit/ArgumentUnpackingTest.class.php | 2 +- .../unittest/emit/ArrayTypesTest.class.php | 2 +- .../ast/unittest/emit/ArraysTest.class.php | 4 ++-- .../ast/unittest/emit/BlockTest.class.php | 2 +- .../ast/unittest/emit/BracesTest.class.php | 2 +- .../emit/CallableSyntaxTest.class.php | 2 +- .../ast/unittest/emit/CastingTest.class.php | 2 +- .../unittest/emit/ClassLiteralTest.class.php | 2 +- .../ast/unittest/emit/CommentsTest.class.php | 2 +- .../emit/ControlStructuresTest.class.php | 6 +++--- .../unittest/emit/DeclarationTest.class.php | 2 +- .../ast/unittest/emit/DeclareTest.class.php | 6 +++--- .../lang/ast/unittest/emit/EchoTest.class.php | 2 +- .../unittest/emit/EmitterTraitTest.class.php | 4 ++-- .../ast/unittest/emit/EmittingTest.class.php | 2 +- .../lang/ast/unittest/emit/EnumTest.class.php | 8 ++++---- .../ast/unittest/emit/EscapingTest.class.php | 2 +- .../unittest/emit/ExceptionsTest.class.php | 2 +- .../unittest/emit/FunctionTypesTest.class.php | 2 +- .../unittest/emit/GeneratedCodeTest.class.php | 6 +++--- .../lang/ast/unittest/emit/GotoTest.class.php | 2 +- .../ast/unittest/emit/ImportTest.class.php | 2 +- .../InitializeWithExpressionsTest.class.php | 6 +++--- .../unittest/emit/InstanceOfTest.class.php | 2 +- .../unittest/emit/InstantiationTest.class.php | 2 +- .../emit/IntersectionTypesTest.class.php | 12 +++++------ .../unittest/emit/InvocationTest.class.php | 8 ++++---- .../ast/unittest/emit/LambdasTest.class.php | 8 ++++---- .../ast/unittest/emit/LoopsTest.class.php | 2 +- .../ast/unittest/emit/MembersTest.class.php | 2 +- .../unittest/emit/MultipleCatchTest.class.php | 2 +- .../unittest/emit/NamespacesTest.class.php | 2 +- .../emit/NullCoalesceAssignmentTest.class.php | 2 +- .../unittest/emit/NullCoalesceTest.class.php | 2 +- .../ast/unittest/emit/NullSafeTest.class.php | 2 +- .../emit/OmitConstModifiersTest.class.php | 4 ++-- .../ast/unittest/emit/OmitTypesTest.class.php | 2 +- .../ast/unittest/emit/PHP81Test.class.php | 2 +- .../ast/unittest/emit/PHP82Test.class.php | 2 +- .../ast/unittest/emit/ParameterTest.class.php | 8 ++++---- .../unittest/emit/PrecedenceTest.class.php | 2 +- .../unittest/emit/PropertyTypesTest.class.php | 2 +- .../ast/unittest/emit/ReadonlyTest.class.php | 20 +++++++++---------- .../unittest/emit/ReflectionTest.class.php | 10 +++++----- .../ast/unittest/emit/ReturnTest.class.php | 2 +- .../emit/RewriteClassOnObjectsTest.class.php | 4 ++-- .../RewriteLambdaExpressionsTest.class.php | 4 ++-- .../emit/RewriteMultiCatchTest.class.php | 4 ++-- .../ast/unittest/emit/ScalarsTest.class.php | 2 +- .../unittest/emit/SyntaxErrorsTest.class.php | 10 +++++----- .../ast/unittest/emit/TernaryTest.class.php | 2 +- .../emit/TrailingCommasTest.class.php | 2 +- .../ast/unittest/emit/TraitsTest.class.php | 6 +++--- .../emit/TransformationsTest.class.php | 2 +- .../emit/TypeDeclarationTest.class.php | 2 +- .../emit/UnicodeEscapesTest.class.php | 2 +- .../unittest/emit/UnionTypesTest.class.php | 14 ++++++------- .../ast/unittest/emit/UsingTest.class.php | 2 +- .../ast/unittest/emit/VarargsTest.class.php | 2 +- .../emit/VirtualPropertyTypesTest.class.php | 14 ++++++------- .../ast/unittest/emit/YieldTest.class.php | 2 +- .../loader/CompilingClassLoaderTest.class.php | 10 +++++----- 80 files changed, 168 insertions(+), 168 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceca1e9d..645ef0ef 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: run: git config --system core.autocrlf false; git config --system core.eol lf - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up PHP ${{ matrix.php-versions }} uses: shivammathur/setup-php@v2 @@ -43,7 +43,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2.1.3 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -56,4 +56,4 @@ jobs: echo "vendor/autoload.php" > composer.pth - name: Run test suite - run: sh xp-run xp.unittest.TestRunner src/test/php + run: sh xp-run xp.test.Runner src/test/php diff --git a/composer.json b/composer.json index 5c312305..2be195bd 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/unittest": "^11.0 | ^10.0" + "xp-framework/test": "dev-main" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 347ff56e..23050040 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -1,10 +1,10 @@ literal($type)); } - #[Test, Values('php71')] + #[Test, Values(from: 'php71')] public function php71_literals($type, $literal) { Assert::equals($literal, (new PHP71())->literal($type)); } - #[Test, Values('php72')] + #[Test, Values(from: 'php72')] public function php72_literals($type, $literal) { Assert::equals($literal, (new PHP72())->literal($type)); } - #[Test, Values('php74')] + #[Test, Values(from: 'php74')] public function php74_literals($type, $literal) { Assert::equals($literal, (new PHP74())->literal($type)); } - #[Test, Values('php80')] + #[Test, Values(from: 'php80')] public function php80_literals($type, $literal) { Assert::equals($literal, (new PHP80())->literal($type)); } - #[Test, Values('php81')] + #[Test, Values(from: 'php81')] public function php81_literals($type, $literal) { Assert::equals($literal, (new PHP81())->literal($type)); } - #[Test, Values('php82')] + #[Test, Values(from: 'php82')] public function php82_literals($type, $literal) { Assert::equals($literal, (new PHP82())->literal($type)); } diff --git a/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php index c62a8da3..f5590344 100755 --- a/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php @@ -1,6 +1,6 @@ folder->getURI()); } - #[Test, Values('files')] + #[Test, Values(from: 'files')] public function iteration($expected) { $results= []; foreach (new FromFilesIn($this->folder) as $path => $stream) { diff --git a/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php index 8c3e09c8..1832bbf0 100755 --- a/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php @@ -1,7 +1,7 @@ $stream) { diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php index dc58270e..90d2b2c3 100755 --- a/src/test/php/lang/ast/unittest/cli/InputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -2,9 +2,9 @@ use io\{File, Folder}; use lang\{Environment, IllegalArgumentException}; -use unittest\{Assert, After, Before, Expect, Test, Values}; +use test\{After, Assert, Before, Expect, Test, Values}; use util\cmd\Console; -use xp\compiler\{Input, FromStream, FromFile, FromFilesIn, FromInputs}; +use xp\compiler\{FromFile, FromFilesIn, FromInputs, FromStream, Input}; class InputTest { private $folder, $file; diff --git a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php index 4821b25e..c4369971 100755 --- a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php @@ -2,9 +2,9 @@ use io\{File, Folder}; use lang\Environment; -use unittest\{Assert, After, Before, Test, Values}; +use test\{After, Assert, Before, Test, Values}; use util\cmd\Console; -use xp\compiler\{Output, CompileOnly, ToStream, ToFile, ToArchive, ToFolder}; +use xp\compiler\{CompileOnly, Output, ToArchive, ToFile, ToFolder, ToStream}; class OutputTest { private $folder, $file, $archive; diff --git a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php index 6b2067ef..53c19921 100755 --- a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php @@ -3,7 +3,7 @@ use io\{File, Folder}; use lang\Environment; use lang\archive\ArchiveClassLoader; -use unittest\{After, Assert, Before, Test}; +use test\{After, Assert, Before, Test}; use xp\compiler\ToArchive; class ToArchiveTest { diff --git a/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php index 72be2ef4..43c2bef1 100755 --- a/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php @@ -3,7 +3,7 @@ use io\{File, Folder}; use lang\Environment; use lang\FileSystemClassLoader; -use unittest\{After, Assert, Before, Test}; +use test\{After, Assert, Before, Test}; use xp\compiler\ToFile; class ToFileTest { diff --git a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php index 0c6d3c61..b3fe466c 100755 --- a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php @@ -1,9 +1,9 @@ type('class { public function __construct(private string... $in) { } diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index cbf78e8c..61dd3b98 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -1,6 +1,6 @@ run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php index 37f58f37..d5eeee37 100755 --- a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php @@ -1,6 +1,6 @@ run('class { public function run($arg) { @@ -167,7 +167,7 @@ public function run($arg) { }', SEEK_END); } - #[Test, Expect(class: Throwable::class, withMessage: '/Unknown seek mode .+/')] + #[Test, Expect(class: Throwable::class, message: '/Unknown seek mode .+/')] public function match_with_throw_expression() { $this->run('class { public function run($arg) { diff --git a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php index 2c674f47..8cf02941 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php @@ -3,7 +3,7 @@ use lang\ast\emit\Declaration; use lang\ast\nodes\{ClassDeclaration, Property}; use lang\ast\types\IsValue; -use unittest\{Assert, Before, Test, Expect}; +use test\{Assert, Before, Expect, Test}; class DeclarationTest { private $type; diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php index 860ebd35..fd5623ed 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -1,7 +1,7 @@ run('declare(strict_types = 1); class { public static function number(int $n) { return $n; } public function run() { return self::number("1"); } }'); } -} +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index a6f1d87a..97b3215f 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -1,6 +1,6 @@ function_exists("enum_exists"))')] +#[Condition(assert: 'fn() => function_exists("enum_exists")')] class EnumTest extends EmittingTest { #[Test] @@ -138,7 +138,7 @@ public function backed_enum_from_string($arg, $expected) { Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); } - #[Test, Expect(class: Error::class, withMessage: '/"illegal" is not a valid backing value for enum .+/')] + #[Test, Expect(class: Error::class, message: '/"illegal" is not a valid backing value for enum .+/')] public function backed_enum_from_nonexistant() { $t= $this->type('enum : string { case ASC = "asc"; diff --git a/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php index 9a9e8e31..946b025c 100755 --- a/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php @@ -2,7 +2,7 @@ use io\streams\MemoryOutputStream; use lang\ast\emit\Escaping; -use unittest\{Assert, Test}; +use test\{Assert, Test}; class EscapingTest { diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index f55c529d..77ca0181 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -1,7 +1,7 @@ run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { const INITIAL= "initial"; @@ -39,7 +39,7 @@ public function run() { }', $declaration))); } - #[Test, Values('expressions')] + #[Test, Values(from: 'expressions')] public function reflective_access_to_property($declaration, $expected) { Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { const INITIAL= "initial"; diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index 0a62b39a..1659ceb3 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -1,6 +1,6 @@ =8.1.0-dev")')] + #[Test, Runtime(php: '>=8.1.0-dev')] public function field_type_restriction_with_php81() { $t= $this->type('class { private Traversable&Countable $test; @@ -59,7 +59,7 @@ public function field_type_restriction_with_php81() { ); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] + #[Test, Runtime(php: '>=8.1.0-dev')] public function parameter_type_restriction_with_php81() { $t= $this->type('class { public function test(Traversable&Countable $arg) { } @@ -71,7 +71,7 @@ public function test(Traversable&Countable $arg) { } ); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.1.0-dev")')] + #[Test, Runtime(php: '>=8.1.0-dev')] public function return_type_restriction_with_php81() { $t= $this->type('class { public function test(): Traversable&Countable { } diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index 17ac561f..b5ad3360 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -1,7 +1,7 @@ =8.0")')] + #[Test, Runtime(php: '>=8.0')] public function named_arguments_in_reverse_order() { Assert::equals('html(<) = &lt;', $this->run( 'class { @@ -155,7 +155,7 @@ public function run() { )); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.0")')] + #[Test, Runtime(php: '>=8.0')] public function named_arguments_omitting_one() { Assert::equals('html(<) = <', $this->run( 'class { diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index d118bb24..206f9265 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -2,8 +2,8 @@ use lang\ast\Errors; use lang\ast\nodes\{ClosureExpression, LambdaExpression}; -use unittest\actions\VerifyThat; -use unittest\{Action, Assert, Expect, Test}; +use test\verify\Condition; +use test\{Action, Assert, Expect, Test}; /** * Lambdas (a.k.a. arrow functions) support @@ -48,7 +48,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[Test, Action(eval: 'new VerifyThat(fn() => property_exists(LambdaExpression::class, "static"))')] + #[Test, Condition(assert: 'fn() => property_exists(LambdaExpression::class, "static")')] public function static_fn_does_not_capture_this() { $r= $this->run('class { public function run() { @@ -59,7 +59,7 @@ public function run() { Assert::false($r()); } - #[Test, Action(eval: 'new VerifyThat(fn() => property_exists(ClosureExpression::class, "static"))')] + #[Test, Condition(assert: 'fn() => property_exists(ClosureExpression::class, "static")')] public function static_function_does_not_capture_this() { $r= $this->run('class { public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index ff8d74fa..ecba2112 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -1,6 +1,6 @@ param('$param')->getType()); } - #[Test, Values('special')] + #[Test, Values(from: 'special')] public function with_special_type($declaration, $type) { Assert::equals($type, $this->param($declaration)->getType()); } @@ -73,7 +73,7 @@ public function nullable_string_type() { Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getType()); } - #[Test, Action(eval: 'new RuntimeVersion(">=7.1")')] + #[Test, Runtime(php: '>=7.1')] public function nullable_string_type_restriction() { Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getTypeRestriction()); } diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index b89c1c72..27bf9b57 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -1,6 +1,6 @@ newInstance('Test')->fixture); } - #[Test, Values('modifiers')] + #[Test, Values(from: 'modifiers')] public function reading_from_class($modifiers) { $t= $this->type('class { public function __construct('.$modifiers.' readonly string $fixture) { } @@ -99,7 +99,7 @@ public function __construct(protected readonly string $fixture) { } Assert::equals('Test', $i->run()); } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] public function cannot_read_protected() { $t= $this->type('class { public function __construct(protected readonly string $fixture) { } @@ -107,7 +107,7 @@ public function __construct(protected readonly string $fixture) { } $t->newInstance('Test')->fixture; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] public function cannot_write_protected() { $t= $this->type('class { public function __construct(protected readonly string $fixture) { } @@ -115,7 +115,7 @@ public function __construct(protected readonly string $fixture) { } $t->newInstance('Test')->fixture= 'Modified'; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] public function cannot_read_private() { $t= $this->type('class { public function __construct(private readonly string $fixture) { } @@ -123,7 +123,7 @@ public function __construct(private readonly string $fixture) { } $t->newInstance('Test')->fixture; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] public function cannot_write_private() { $t= $this->type('class { public function __construct(private readonly string $fixture) { } @@ -151,7 +151,7 @@ public function can_be_assigned_via_reflection() { Assert::equals('Test', $i->fixture); } - #[Test, Expect(class: Error::class, withMessage: '/Cannot initialize readonly property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot initialize readonly property .+fixture/')] public function cannot_initialize_from_outside() { $t= $this->type('class { public readonly string $fixture; @@ -159,7 +159,7 @@ public function cannot_initialize_from_outside() { $t->newInstance()->fixture= 'Test'; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot modify readonly property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] public function cannot_be_set_after_initialization() { $t= $this->type('class { public function __construct(public readonly string $fixture) { } @@ -174,13 +174,13 @@ public function cannot_have_an_initial_value() { }'); } - #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] public function cannot_read_dynamic_members_from_readonly_classes() { $t= $this->type('readonly class { }'); $t->newInstance()->fixture; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot create dynamic property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] public function cannot_write_dynamic_members_from_readonly_classes() { $t= $this->type('readonly class { }'); $t->newInstance()->fixture= true; diff --git a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php index eb279197..e03fa8ff 100755 --- a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php @@ -1,9 +1,9 @@ rewriteEnumCase('EMPTY')); } - #[Test, Action(eval: 'new RuntimeVersion("<8.1")')] + #[Test, Runtime(php: '<8.1')] public function rewrites_simulated_unit_enums() { $spec= ['kind' => 'class', 'extends' => null, 'implements' => [\UnitEnum::class], 'use' => []]; $t= ClassLoader::defineType('ReflectionTestSimulatedEnum', $spec, '{ @@ -58,7 +58,7 @@ static function __static() { Assert::false($reflect->rewriteEnumCase('EMPTY')); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.1")')] + #[Test, Runtime(php: '>=8.1')] public function does_not_rewrite_native_enums() { $spec= ['kind' => 'enum', 'extends' => null, 'implements' => [], 'use' => []]; $t= ClassLoader::defineType('ReflectionTestNativeEnum', $spec, '{ diff --git a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php index d15ddf3b..6b1ed68b 100755 --- a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php @@ -1,6 +1,6 @@ emit('$greeting= hello() world()'); } - #[Test, Expect(class: Errors::class, withMessage: 'Unexpected :')] + #[Test, Expect(class: Errors::class, message: 'Unexpected :')] public function unexpected_colon() { $this->emit('$greeting= hello();:'); } - #[Test, Expect(class: IllegalStateException::class, withMessage: 'Unexpected operator = at line 1')] + #[Test, Expect(class: IllegalStateException::class, message: 'Unexpected operator = at line 1')] public function operator() { $this->emit('=;'); } -} +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 7bffb2a7..b397dd06 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -1,7 +1,7 @@ getMethod('noLongerSpinning')->invoke($instance)); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.2.0-dev")')] + #[Test, Runtime(php: '>=8.2.0-dev')] public function can_have_constants() { $t= $this->type('trait { const FIXTURE = 1; }'); Assert::equals(1, $t->getConstant('FIXTURE')); diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index a55e33df..8dc2f47f 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -3,7 +3,7 @@ use lang\ast\Code; use lang\ast\nodes\{Method, Signature}; use lang\ast\types\IsLiteral; -use unittest\{Assert, Before, Test, Values}; +use test\{Assert, Before, Test, Values}; class TransformationsTest extends EmittingTest { diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index 508167cb..b88813ac 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -2,7 +2,7 @@ use lang\XPClass; use lang\reflect\Modifiers; -use unittest\{Assert, Test, Values}; +use test\{Assert, Test, Values}; class TypeDeclarationTest extends EmittingTest { diff --git a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php index d0c21758..2ade1e33 100755 --- a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php @@ -1,6 +1,6 @@ =8.0.0-dev")')] + #[Test, Runtime(php: '>=8.0.0-dev')] public function nullable_union_type_restriction() { $t= $this->type('class { public function test(): int|string|null { } @@ -83,7 +83,7 @@ public function test(): int|string|null { } ); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + #[Test, Runtime(php: '>=8.0.0-dev')] public function parameter_type_restriction_with_php8() { $t= $this->type('class { public function test(int|string|array $arg) { } @@ -95,7 +95,7 @@ public function test(int|string|array $arg) { } ); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + #[Test, Runtime(php: '>=8.0.0-dev')] public function parameter_function_type_restriction_with_php8() { $t= $this->type('class { public function test(): string|(function(): string) { } @@ -107,7 +107,7 @@ public function test(): string|(function(): string) { } ); } - #[Test, Action(eval: 'new RuntimeVersion(">=8.0.0-dev")')] + #[Test, Runtime(php: '>=8.0.0-dev')] public function return_type_restriction_with_php8() { $t= $this->type('class { public function test(): int|string|array { } diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index 0dc7e0d5..fd382d29 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -1,6 +1,6 @@ getField('value')->getModifiers()); } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+::\\$value/')] + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] public function cannot_read_private_field() { $t= $this->type('class { private int $value; @@ -36,7 +36,7 @@ public function cannot_read_private_field() { $t->newInstance()->value; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access private property .+::\\$value/')] + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] public function cannot_write_private_field() { $t= $this->type('class { private int $value; @@ -45,7 +45,7 @@ public function cannot_write_private_field() { $t->newInstance()->value= 6100; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+::\\$value/')] + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] public function cannot_read_protected_field() { $t= $this->type('class { protected int $value; @@ -54,7 +54,7 @@ public function cannot_read_protected_field() { $t->newInstance()->value; } - #[Test, Expect(class: Error::class, withMessage: '/Cannot access protected property .+::\\$value/')] + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] public function cannot_write_protected_field() { $t= $this->type('class { protected int $value; @@ -87,7 +87,7 @@ public function initial_value_available_via_reflection() { Assert::equals(6100, $t->getField('value')->setAccessible(true)->get($t->newInstance())); } - #[Test, Values([[null], ['Test'], [[]]]), Expect(class: Error::class, withMessage: '/property .+::\$value of type int/')] + #[Test, Values([[null], ['Test'], [[]]]), Expect(class: Error::class, message: '/property .+::\$value of type int/')] public function type_checked_at_runtime($in) { $this->run('class { private int $value; diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index 1ba186aa..6214402c 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -1,6 +1,6 @@ getSimpleName()); } - #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "type name", have "(end)"')] + #[Test, Expect(class: ClassFormatException::class, message: 'Compiler error: Expected "type name", have "(end)"')] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); }); } - #[Test, Action(eval: 'new RuntimeVersion(">=7.3")'), Expect(class: ClassFormatException::class, withMessage: '/Compiler error: Class .+ not found/')] + #[Test, Runtime(php: '>=7.3'), Expect(class: ClassFormatException::class, message: '/Compiler error: Class .+ not found/')] public function load_class_with_non_existant_parent() { $code= "compile(['Orphan' => $code], function($loader, $types) { From 21aa543c25f35121705daef31f4b6557c23a16e6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 3 Feb 2023 22:29:17 +0100 Subject: [PATCH 680/926] QA: Simplify #[Condition] --- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/LambdasTest.class.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 6d99c994..9c5019e5 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -5,7 +5,7 @@ use test\verify\Condition; use test\{Action, Assert, Expect, Ignore, Test, Values}; -#[Condition(assert: 'fn() => function_exists("enum_exists")')] +#[Condition(assert: 'function_exists("enum_exists")')] class EnumTest extends EmittingTest { #[Test] diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 206f9265..8a0a1db0 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -48,7 +48,7 @@ public function run() { Assert::equals(3, $r(1)); } - #[Test, Condition(assert: 'fn() => property_exists(LambdaExpression::class, "static")')] + #[Test, Condition(assert: 'property_exists(LambdaExpression::class, "static")')] public function static_fn_does_not_capture_this() { $r= $this->run('class { public function run() { @@ -59,7 +59,7 @@ public function run() { Assert::false($r()); } - #[Test, Condition(assert: 'fn() => property_exists(ClosureExpression::class, "static")')] + #[Test, Condition(assert: 'property_exists(ClosureExpression::class, "static")')] public function static_function_does_not_capture_this() { $r= $this->run('class { public function run() { From a3aae9c11133aab60506d94c819066d001d07e1b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Feb 2023 21:28:26 +0100 Subject: [PATCH 681/926] Use patterns for expected exceptions --- .../lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- .../php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php | 6 +++--- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 05925b71..cbe3700b 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -83,7 +83,7 @@ public function __construct(private int $id, private string $name) { } ); } - #[Test, Expect(class: Errors::class, message: 'Variadic parameters cannot be promoted')] + #[Test, Expect(class: Errors::class, message: '/Variadic parameters cannot be promoted/')] public function variadic_parameters_cannot_be_promoted() { $this->type('class { public function __construct(private string... $in) { } diff --git a/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php index e96b2e1a..d6b3836d 100755 --- a/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php @@ -6,17 +6,17 @@ class SyntaxErrorsTest extends EmittingTest { - #[Test, Expect(class: Errors::class, message: 'Missing semicolon after assignment statement')] + #[Test, Expect(class: Errors::class, message: '/Missing semicolon after assignment statement/')] public function missing_semicolon() { $this->emit('$greeting= hello() world()'); } - #[Test, Expect(class: Errors::class, message: 'Unexpected :')] + #[Test, Expect(class: Errors::class, message: '/Unexpected :/')] public function unexpected_colon() { $this->emit('$greeting= hello();:'); } - #[Test, Expect(class: IllegalStateException::class, message: 'Unexpected operator = at line 1')] + #[Test, Expect(class: IllegalStateException::class, message: '/Unexpected operator =/')] public function operator() { $this->emit('=;'); } diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 9f66aa3f..598c8f9d 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -131,7 +131,7 @@ public function load_uri() { Assert::equals('Tests', $class->getSimpleName()); } - #[Test, Expect(class: ClassFormatException::class, message: 'Compiler error: Expected "type name", have "(end)"')] + #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Expected "type name", have .+/')] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); From 6b58e9ca88a6082f4458c65229bb8c4f394f4af3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 7 Feb 2023 21:13:27 +0100 Subject: [PATCH 682/926] Support `--output=ast,code` on command line --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index fc279114..1f107045 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -5,9 +5,10 @@ use lang\ast\emit\GeneratedCode; use lang\ast\emit\php\XpMeta; use lang\ast\{CompilingClassLoader, Emitter, Language, Result, Tokens}; -use test\{After, Assert, TestCase}; +use test\{Args, After, Assert, TestCase}; use util\cmd\Console; +#[Args(['output' => null])] abstract class EmittingTest { private static $id= 0; private $cl, $language, $emitter, $output; From 37569bdb67201b53a6f50c5fd39fffa1f8cbd7f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Feb 2023 12:21:55 +0100 Subject: [PATCH 683/926] Use xp-framework/test release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2be195bd..83462d56 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/test": "dev-main" + "xp-framework/test": "^1.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { From 5b7e8eb4bdc00bafaf27dfa7e29c8c743d25fab1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Feb 2023 17:48:09 +0100 Subject: [PATCH 684/926] Document new testing library --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 0c9314cd..f0b0eb3b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #153: Migrate to new testing library - @thekid + ## 8.8.1 / 2023-01-29 * Added flag to meta information to resolve ambiguity when exactly one From b17b99e923f4ac808a373ab1c9b328af546c3302 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Feb 2023 23:19:45 +0100 Subject: [PATCH 685/926] Use declaration() to emit __init() call Slightly shorter generated code --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1a94db1d..4f82d947 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -379,7 +379,7 @@ protected function emitEnum($result, $enum) { $result->out->write('static function __init() {'); $this->emitInitializations($result, $context->statics); $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); - $result->out->write('}} '.$enum->name->literal().'::__init();'); + $result->out->write('}} '.$enum->declaration().'::__init();'); $result->codegen->leave(); } @@ -444,7 +444,7 @@ protected function emitClass($result, $class) { $result->out->write('static function __init() {'); $this->emitInitializations($result, $context->statics); $this->emitMeta($result, $class->name, $class->annotations, $class->comment); - $result->out->write('}} '.$class->name->literal().'::__init();'); + $result->out->write('}} '.$class->declaration().'::__init();'); $result->codegen->leave(); } From af893700b4b3a55ea6b719f1e22abc5785044b50 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Feb 2023 23:23:50 +0100 Subject: [PATCH 686/926] Add back PHP 8.3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 645ef0ef..ca5ee1de 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] os: [ubuntu-latest, windows-latest] steps: From cde4dc88a3066c614460264587dfbd1030f7bb96 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 11 Feb 2023 23:41:52 +0100 Subject: [PATCH 687/926] Remove problem matchers for the moment --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca5ee1de..4fdfe444 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,6 @@ jobs: php-version: ${{ matrix.php-versions }} ini-values: date.timezone=Europe/Berlin - - name: Setup Problem Matchers for PHP - run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - name: Validate composer.json and composer.lock run: composer validate From 76dcd321499f5b88a91e1e15b9b9c68941ffead7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 12:04:57 +0100 Subject: [PATCH 688/926] Fix emitted meta information for generic declarations --- ChangeLog.md | 1 + src/main/php/lang/ast/emit/php/XpMeta.class.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index f0b0eb3b..20a9dea5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed emitted meta information for generic declarations - @thekid * Merged PR #153: Migrate to new testing library - @thekid ## 8.8.1 / 2023-01-29 diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index 4c445551..18e514d9 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -1,5 +1,7 @@ out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); + } else if ($type instanceof IsGeneric) { + $result->out->write('\xp::$meta[\''.$type->base->name().'\']= ['); } else { - $result->out->write('\xp::$meta[\''.strtr($type->name(), '\\', '.').'\']= ['); + $result->out->write('\xp::$meta[\''.$type->name().'\']= ['); } $result->out->write('"class" => ['); $this->attributes($result, $annotations, []); From 11193752a7ac2911d99b69907ed5fc3d544eb03e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 12:05:31 +0100 Subject: [PATCH 689/926] Release 8.8.2 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 20a9dea5..0c19c4d6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.2 / 2023-02-12 + * Fixed emitted meta information for generic declarations - @thekid * Merged PR #153: Migrate to new testing library - @thekid From 2e277ca53c47808ea7d56d971b6adb769fa2dd57 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 13:49:34 +0100 Subject: [PATCH 690/926] Do not error at compile time for unresolved classes --- .../php/lang/ast/emit/GeneratedCode.class.php | 6 +++++- .../php/lang/ast/emit/Incomplete.class.php | 21 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 6 +++++- .../unittest/emit/GeneratedCodeTest.class.php | 6 +++--- 4 files changed, 34 insertions(+), 5 deletions(-) create mode 100755 src/main/php/lang/ast/emit/Incomplete.class.php diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 558c0a42..9c1716af 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -80,6 +80,10 @@ public function lookup($type) { } } - return new Reflection($type); + if (class_exists($type) || interface_exists($type) || trait_exists($type) || enum_exists($type)) { + return new Reflection($type); + } else { + return new Incomplete($type); + } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php new file mode 100755 index 00000000..f4fcaffb --- /dev/null +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -0,0 +1,21 @@ +name= $name; } + + /** @return string */ + public function name() { return $this->name; } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function rewriteEnumCase($member) { + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 4f82d947..b5d007dc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -994,7 +994,11 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression)) { + } else if ( + $scope->member instanceof Literal && + 'class' !== $scope->member->literal && + $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression) + ) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 2047ba06..87a9ff3a 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -1,7 +1,7 @@ lookup('\\lang\\Value')); } - #[Test, Expect(ClassNotFoundException::class)] + #[Test] public function lookup_non_existant() { $r= new GeneratedCode(new MemoryOutputStream()); - $r->lookup('\\NotFound'); + Assert::instance(Incomplete::class, $r->lookup('\\NotFound')); } #[Test] From 1dd754c0f8797209857bda04fbb68ba765652da3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 14:12:23 +0100 Subject: [PATCH 691/926] Advertise new reflection API --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ac11037..523a8775 100755 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The following code uses Hack language, PHP 8.2, 8.1, 8.0, PHP 7.4, PHP 7.3, PHP ```php $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; - $author= Type::forName(self::class)->getAnnotation('author'); + $author= Reflection::type(self::class)->annotation(Author::class)->argument(0); Console::writeLine($greet($args[0] ?? 'World', from: $author)); } From 8e3c67004ca1fbefa2a52299a9ec814698952fa0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 14:13:39 +0100 Subject: [PATCH 692/926] Release 8.8.3 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 0c19c4d6..b69620e7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.3 / 2023-02-12 + +* Merged PR #156: Do not error at compile time for unresolved classes + (@thekid) + ## 8.8.2 / 2023-02-12 * Fixed emitted meta information for generic declarations - @thekid From d63da3c65aa68df6830a3dd17d2ae1df4cd70186 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 14:18:22 +0100 Subject: [PATCH 693/926] Fix reference to #155 [skip ci] --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index b69620e7..ca4cf78c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,7 @@ XP Compiler ChangeLog ## 8.8.3 / 2023-02-12 -* Merged PR #156: Do not error at compile time for unresolved classes +* Merged PR #155: Do not error at compile time for unresolved classes (@thekid) ## 8.8.2 / 2023-02-12 From 8f68dba0c8940b14559ccb4d767beff5ee2335f6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 14:25:20 +0100 Subject: [PATCH 694/926] Include `-m /path/to/xp/reflection` [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 523a8775..bdbabba2 100755 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ class HelloWorld { } ``` -To run this code, use `xp HelloWorld` in a terminal window. +To run this code, use `xp -m /path/to/xp/reflection HelloWorld` in a terminal window. Compilation ----------- From 18872f803aec397ff3f8ce63beeaf398c354b5c0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 12 Feb 2023 14:32:49 +0100 Subject: [PATCH 695/926] Elaborate on other features --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bdbabba2..df2e76a3 100755 --- a/README.md +++ b/README.md @@ -75,7 +75,9 @@ Features supported XP Compiler supports features such as annotations, arrow functions, enums, property type-hints, the null-safe instance operator as well as all PHP 7 and PHP 8 syntax additions. A complete list including examples can be found [in our Wiki](https://github.com/xp-framework/compiler/wiki). -Additional syntax can be added by installing compiler plugins from [here](https://github.com/xp-lang): +### More features + +Additional syntax like an `is` operator, generics or record types can be added by installing compiler plugins from [here](https://github.com/xp-lang): ```bash $ composer require xp-lang/php-is-operator From dc0da5d8ab2f48cbad55dda064c1d4829feef049 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 16 Feb 2023 21:16:03 +0100 Subject: [PATCH 696/926] Fix "Undefined property: lang\ast\nodes\Literal::$literal" --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b5d007dc..d2b01e51 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -996,7 +996,7 @@ protected function emitScope($result, $scope) { $result->out->write(':null'); } else if ( $scope->member instanceof Literal && - 'class' !== $scope->member->literal && + 'class' !== $scope->member->expression && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression) ) { $result->out->write($scope->type.'::$'.$scope->member->expression); From cad16ee3fed24cd45a76323b08ce949249de8459 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 17 Feb 2023 13:58:51 +0100 Subject: [PATCH 697/926] Add tests for typed properties and constants See https://wiki.php.net/rfc/typed_class_constants --- .../ast/unittest/emit/MembersTest.class.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 26db04e8..412db002 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -18,6 +18,19 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function typed_class_property() { + $r= $this->run('class { + private static string $MEMBER= "Test"; + + public function run() { + return self::$MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + #[Test] public function class_method() { $r= $this->run('class { @@ -44,6 +57,19 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function typed_class_constant() { + $r= $this->run('class { + private const string MEMBER = "Test"; + + public function run() { + return self::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + #[Test] public function dynamic_class_property() { $r= $this->run('class { @@ -154,6 +180,19 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function typed_instance_property() { + $r= $this->run('class { + private string $member= "Test"; + + public function run() { + return $this->member; + } + }'); + + Assert::equals('Test', $r); + } + #[Test] public function instance_method() { $r= $this->run('class { From daa311aa7317875aca8cf593a9b591d5e05d8808 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 11:20:26 +0100 Subject: [PATCH 698/926] Release 8.8.4 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index ca4cf78c..a5d7eaa5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.4 / 2023-02-19 + +* Fixed optimization for `::class` constant resolution - @thekid + ## 8.8.3 / 2023-02-12 * Merged PR #155: Do not error at compile time for unresolved classes From f5c3a7e78d1c71b0ed4a57d970ea214aa55e63dc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 13:11:06 +0100 Subject: [PATCH 699/926] Implement command line option to set file extension for compiled files --- src/main/php/xp/compiler/CompileRunner.class.php | 5 ++++- src/main/php/xp/compiler/Output.class.php | 12 ++++++++++++ src/main/php/xp/compiler/ToArchive.class.php | 16 +++++++++++----- src/main/php/xp/compiler/ToFolder.class.php | 4 ++-- .../lang/ast/unittest/cli/ToFolderTest.class.php | 4 ++-- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index b776a8fb..88563d1b 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -69,6 +69,7 @@ public static function main(array $args) { $in= $out= '-'; $quiet= false; $augment= []; + $ext= \xp::CLASS_FILE_EXT; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { $target= $args[++$i]; @@ -78,6 +79,8 @@ public static function main(array $args) { $out= $args[++$i]; $in= array_slice($args, $i + 1); break; + } else if ('-e' === $args[$i]) { + $ext= $args[++$i]; } else if ('-n' === $args[$i]) { $out= null; $in= array_slice($args, $i + 1); @@ -98,7 +101,7 @@ public static function main(array $args) { } $input= Input::newInstance($in); - $output= Output::newInstance($out); + $output= Output::newInstance($out)->using($ext); $t= new Timer(); $total= $errors= 0; diff --git a/src/main/php/xp/compiler/Output.class.php b/src/main/php/xp/compiler/Output.class.php index 82be96d8..75b8837d 100755 --- a/src/main/php/xp/compiler/Output.class.php +++ b/src/main/php/xp/compiler/Output.class.php @@ -3,6 +3,7 @@ use util\cmd\Console; abstract class Output { + protected $extension= \xp::CLASS_FILE_EXT; /** * Returns output from the command line argument @@ -24,6 +25,17 @@ public static function newInstance($arg) { } } + /** + * Change file extension, which defaults to `xp::CLASS_FILE_EXT`. + * + * @param string $extension + * @return self + */ + public function using($extension) { + $this->extension= '.' === $extension[0] ? $extension : '.'.$extension; + return $this; + } + /** * Returns the target for a given input * diff --git a/src/main/php/xp/compiler/ToArchive.class.php b/src/main/php/xp/compiler/ToArchive.class.php index 6c10a558..91a3a65a 100755 --- a/src/main/php/xp/compiler/ToArchive.class.php +++ b/src/main/php/xp/compiler/ToArchive.class.php @@ -21,15 +21,21 @@ public function __construct($file) { * @return io.streams.OutputStream */ public function target($name) { - return new class($this->archive, $name) implements OutputStream { - private static $replace= [DIRECTORY_SEPARATOR => '/', '.php' => \xp::CLASS_FILE_EXT]; + return new class($this->archive, $name, $this->extension) implements OutputStream { + private $archive, $name, $replace; private $bytes= ''; - private $archive, $name; - public function __construct($archive, $name) { $this->archive= $archive; $this->name= $name; } + public function __construct($archive, $name, $extension) { + $this->archive= $archive; + $this->name= $name; + $this->replace= [DIRECTORY_SEPARATOR => '/', '.php' => $extension]; + } + public function write($bytes) { $this->bytes.= $bytes; } + public function flush() { } - public function close() { $this->archive->addBytes(strtr($this->name, self::$replace), $this->bytes); } + + public function close() { $this->archive->addBytes(strtr($this->name, $this->replace), $this->bytes); } }; } diff --git a/src/main/php/xp/compiler/ToFolder.class.php b/src/main/php/xp/compiler/ToFolder.class.php index c1f11256..56773dd2 100755 --- a/src/main/php/xp/compiler/ToFolder.class.php +++ b/src/main/php/xp/compiler/ToFolder.class.php @@ -18,9 +18,9 @@ public function __construct($folder) { */ public function target($name) { if ('-' === $name) { - $f= new File($this->folder, 'out'.\xp::CLASS_FILE_EXT); + $f= new File($this->folder, 'out'.$this->extension); } else { - $f= new File($this->folder, str_replace('.php', \xp::CLASS_FILE_EXT, $name)); + $f= new File($this->folder, str_replace('.php', $this->extension, $name)); } $this->ensure($f->path); return $f->out(); diff --git a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php index b3fe466c..8b054314 100755 --- a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php @@ -28,10 +28,10 @@ public function can_create() { #[Test] public function dash_special_case() { - with ((new ToFolder($this->folder))->target('-'), function($out) { + with ((new ToFolder($this->folder))->using('.php')->target('-'), function($out) { $out->write('folder, 'out'.\xp::CLASS_FILE_EXT))->exists()); + Assert::true((new File($this->folder, 'out.php'))->exists()); } #[Test] From a50b768b3272f79b857ec4b353d9029e2a09a2c1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 13:25:40 +0100 Subject: [PATCH 700/926] Generate labels using the CodeGen::symbol() method This prevents duplicate label names from being emitted, and fixes #160 --- ChangeLog.md | 3 +++ .../lang/ast/emit/RewriteMultiCatch.class.php | 2 +- .../unittest/emit/ExceptionsTest.class.php | 19 +++++++++++++++++++ .../emit/RewriteMultiCatchTest.class.php | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index a5d7eaa5..78a21fb9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed issue #160: Fatal error: Label 'c1107822099' already defined + (@thekid) + ## 8.8.4 / 2023-02-19 * Fixed optimization for `::class` constant resolution - @thekid diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php index ea1bd69e..5088f5f6 100755 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php @@ -13,7 +13,7 @@ protected function emitCatch($result, $catch) { $result->out->write('catch(\\Throwable '.$capture.') {'); } else { $last= array_pop($catch->types); - $label= sprintf('c%u', crc32($last)); + $label= $result->codegen->symbol(); foreach ($catch->types as $type) { $result->out->write('catch('.$type.' '.$capture.') { goto '.$label.'; }'); } diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 77ca0181..0afb163c 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -206,4 +206,23 @@ public function run($message) { }'); $t->newInstance()->run('Test'); } + + #[Test] + public function try_catch_nested_inside_catch() { + $t= $this->type('class { + public function run() { + try { + throw new \\lang\\IllegalArgumentException("test"); + } catch (\\lang\\IllegalArgumentException $expected) { + try { + throw $expected; + } catch (\\lang\\IllegalArgumentException $expected) { + return $expected->getMessage(); + } + } + } + }'); + + Assert::equals('test', $t->newInstance()->run()); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php index e6d769ab..89b3cb9a 100755 --- a/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php @@ -32,7 +32,7 @@ public function rewrites_catch_without_variable() { #[Test] public function rewrites_catch_with_multiple_types_using_goto() { Assert::equals( - 'try {}catch(\\Exception $t) { goto c2427456839; }catch(\\Error $t) { c2427456839:}', + 'try {}catch(\\Exception $t) { goto _0; }catch(\\Error $t) { _0:}', $this->emit(new TryStatement([], [new CatchStatement(['\\Exception', '\\Error'], 't', [])], null)) ); } From 8fcb4887e7903a5feaf8b38f8527fee7c09ad316 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 13:26:29 +0100 Subject: [PATCH 701/926] Release 8.8.5 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 78a21fb9..af9a3dac 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.8.5 / 2023-02-19 + * Fixed issue #160: Fatal error: Label 'c1107822099' already defined (@thekid) From 585f9862ba14ccce42a86b1b05138222c0e0879b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 15:26:57 +0100 Subject: [PATCH 702/926] Document file extension argument --- ChangeLog.md | 5 +++++ src/main/php/xp/compiler/CompileRunner.class.php | 3 +++ 2 files changed, 8 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index af9a3dac..86dda651 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #159: Implement command line option to set file extension + for compiled files. This makes integrating with PSR-4 autoloading an + easy task, see https://www.php-fig.org/psr/psr-4/ + (@thekid) + ## 8.8.5 / 2023-02-19 * Fixed issue #160: Fatal error: Label 'c1107822099' already defined diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 88563d1b..6f3fb3e4 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -40,6 +40,9 @@ * $ xp compile -t php:7.4 -a php:xp-meta -o dist src/main/php * ``` * + * Use *-e* to change the file extension for generated files inside folders + * and archives, which defaults to `.class.php`. + * * The *-o* and *-n* options accept multiple input sources following them. * The *-q* option suppresses all diagnostic output except for errors. * From 2635de6bc0b7496bace39d3c9121717ac76d0ba9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 19 Feb 2023 21:57:25 +0100 Subject: [PATCH 703/926] Release 8.9.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 86dda651..a5bf79df 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.9.0 / 2023-02-19 + * Merged PR #159: Implement command line option to set file extension for compiled files. This makes integrating with PSR-4 autoloading an easy task, see https://www.php-fig.org/psr/psr-4/ From afc10428501746da855cba3caf591d4941b95fdc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 7 Apr 2023 13:58:10 +0200 Subject: [PATCH 704/926] Add a variety of tests for dynamic members --- .../ast/unittest/emit/MembersTest.class.php | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 412db002..6db1e8d2 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,7 +1,7 @@ $member', '$this->{$member}'])] + public function dynamic_instance_property($syntax) { + $r= $this->run('class { + private $MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Ignore('Unsupported!')] public function dynamic_class_property() { + $r= $this->run('class { + private static $MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return self::${$member}; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values(['$this->$method()', '$this->{$method}()'])] + public function dynamic_instance_method($syntax) { + $r= $this->run('class { + private function test() { return "Test"; } + + public function run() { + $method= "test"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values(['self::$method()', 'self::{$method}()'])] + public function dynamic_class_method($syntax) { + $r= $this->run('class { + private static function test() { return "Test"; } + + public function run() { + $method= "test"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Ignore('Unsupported!')] + public function dynamic_class_constant() { + $r= $this->run('class { + const MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return self::{$member}; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function property_of_dynamic_class() { $r= $this->run('class { private static $MEMBER= "Test"; @@ -85,7 +155,7 @@ public function run() { } #[Test] - public function dynamic_class_method() { + public function method_of_dynamic_class() { $r= $this->run('class { private static function member() { return "Test"; } @@ -99,7 +169,7 @@ public function run() { } #[Test] - public function dynamic_class_constant() { + public function constant_of_dynamic_class() { $r= $this->run('class { private const MEMBER = "Test"; From 5dc5d8c3c39ec5bb23ebe23e385f9c3835dc3477 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 16:35:49 +0200 Subject: [PATCH 705/926] Add support for dynamic class constants and dynamic `new` --- composer.json | 2 +- .../ast/emit/ArbitrayNewExpressions.class.php | 4 +- .../ast/emit/CallablesAsClosures.class.php | 6 +- .../ast/emit/NullsafeAsTernaries.class.php | 9 +-- src/main/php/lang/ast/emit/PHP.class.php | 73 +++++++++++-------- src/main/php/lang/ast/emit/PHP70.class.php | 8 +- src/main/php/lang/ast/emit/PHP80.class.php | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- src/main/php/lang/ast/emit/PHP82.class.php | 2 +- .../ast/emit/RewriteClassOnObjects.class.php | 5 +- .../RewriteDynamicClassConstants.class.php | 21 ++++++ .../lang/ast/unittest/EmitterTest.class.php | 13 +++- .../unittest/emit/InstantiationTest.class.php | 24 ++++++ .../ast/unittest/emit/MembersTest.class.php | 18 ++--- 14 files changed, 128 insertions(+), 61 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php diff --git a/composer.json b/composer.json index 83462d56..5869b564 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": "^9.2", + "xp-framework/ast": "dev-feature/expressions as 10.0.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php index 6c332485..d3a6551e 100755 --- a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php +++ b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php @@ -14,8 +14,8 @@ protected function emitNew($result, $new) { if (!($new->type instanceof IsExpression)) return parent::emitNew($result, $new); // Emit supported `new $var`, rewrite unsupported `new ($expr)` - if ($new->type->expression instanceof Variable) { - $result->out->write('new $'.$new->type->expression->name.'('); + if ($new->type->expression instanceof Variable && $new->type->expression->const) { + $result->out->write('new $'.$new->type->expression->pointer.'('); $this->emitArguments($result, $new->arguments); $result->out->write(')'); } else { diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php index e020b858..f2c2ce3f 100755 --- a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -1,7 +1,7 @@ out->write(','); $this->emitQuoted($result, $node->member); $result->out->write(']'); + } else if ($node instanceof Expression) { + + // Rewrite T::{} => [T::class, ] + $this->emitOne($result, $node->inline); } else { // Emit other expressions as-is diff --git a/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php index 3f6838c4..32fe0a5b 100755 --- a/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php +++ b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php @@ -12,13 +12,6 @@ protected function emitNullsafeInstance($result, $instance) { $result->out->write('null===('.$t.'='); $this->emitOne($result, $instance->expression); $result->out->write(')?null:'.$t.'->'); - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } + $this->emitOne($result, $instance->member); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d2b01e51..6842b785 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -7,6 +7,7 @@ BinaryExpression, Block, Comment, + Expression, InstanceExpression, Literal, Property, @@ -104,8 +105,8 @@ protected function propertyType($type) { protected function enclose($result, $node, $signature, $static, $emit) { $capture= []; foreach ($result->codegen->search($node, 'variable') as $var) { - if (isset($result->locals[$var->name])) { - $capture[$var->name]= true; + if (isset($result->locals[$var->pointer])) { + $capture[$var->pointer]= true; } } unset($capture['this']); @@ -228,7 +229,18 @@ protected function emitStatic($result, $static) { } protected function emitVariable($result, $variable) { - $result->out->write('$'.$variable->name); + if ($variable->const) { + $result->out->write('$'.$variable->pointer); + } else { + $result->out->write('$'); + $this->emitOne($result, $variable->pointer); + } + } + + protected function emitExpression($result, $expression) { + $result->out->write('{'); + $this->emitOne($result, $expression->inline); + $result->out->write('}'); } protected function emitCast($result, $cast) { @@ -674,10 +686,10 @@ protected function emitOffset($result, $offset) { } protected function emitAssign($result, $target) { - if ('variable' === $target->kind) { - $result->out->write('$'.$target->name); - $result->locals[$target->name]= true; - } else if ('array' === $target->kind) { + if ($target instanceof Variable && $target->const) { + $result->out->write('$'.$target->pointer); + $result->locals[$target->pointer]= true; + } else if ($target instanceof ArrayLiteral) { $result->out->write('list('); foreach ($target->values as $pair) { if ($pair[0]) { @@ -813,7 +825,8 @@ protected function emitForeach($result, $foreach) { $result->out->write(' => '); } - if ('array' === $foreach->value->kind) { + // Support empty elements: `foreach ( as [$a, , $b])` + if ($foreach->value instanceof ArrayLiteral) { $result->out->write('['); foreach ($foreach->value->values as $pair) { if ($pair[0]) { @@ -960,6 +973,13 @@ protected function emitNewClass($result, $new) { } protected function emitCallable($result, $callable) { + + // Disambiguate the following: + // + // - `T::{$func}`, a dynamic class constant + // - `T::{$func}(...)`, a dynamic first-class callable + $callable->expression->line= -1; + $this->emitOne($result, $callable->expression); $result->out->write('(...)'); } @@ -983,25 +1003,29 @@ protected function emitInvoke($result, $invoke) { } protected function emitScope($result, $scope) { + + // $x:: vs. e.g. invoke():: vs. T:: if ($scope->type instanceof Variable) { $this->emitOne($result, $scope->type); $result->out->write('::'); - $this->emitOne($result, $scope->member); } else if ($scope->type instanceof Node) { $t= $result->temp(); - $result->out->write('('.$t.'='); + $result->out->write('(null==='.$t.'='); $this->emitOne($result, $scope->type); - $result->out->write(')?'.$t.'::'); - $this->emitOne($result, $scope->member); - $result->out->write(':null'); - } else if ( + $result->out->write(")?null:{$t}::"); + } else { + $result->out->write("{$scope->type}::"); + } + + // Rewrite T::member to T::$member for XP enums + if ( $scope->member instanceof Literal && + is_string($scope->type) && 'class' !== $scope->member->expression && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression) ) { - $result->out->write($scope->type.'::$'.$scope->member->expression); + $result->out->write('$'.$scope->member->expression); } else { - $result->out->write($scope->type.'::'); $this->emitOne($result, $scope->member); } } @@ -1016,26 +1040,13 @@ protected function emitInstance($result, $instance) { $result->out->write('->'); } - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } + $this->emitOne($result, $instance->member); } protected function emitNullsafeInstance($result, $instance) { $this->emitOne($result, $instance->expression); $result->out->write('?->'); - - if ('literal' === $instance->member->kind) { - $result->out->write($instance->member->expression); - } else { - $result->out->write('{'); - $this->emitOne($result, $instance->member); - $result->out->write('}'); - } + $this->emitOne($result, $instance->member); } protected function emitUnpack($result, $unpack) { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 14009e53..e2425630 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -1,7 +1,7 @@ emitOne($result, $callable->expression->expression); if ($callable->expression->member instanceof Literal) { $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else if ($callable->expression->member instanceof Expression) { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member->inline); } else { $result->out->write(','); $this->emitOne($result, $callable->expression->member); @@ -90,6 +93,9 @@ protected function emitCallable($result, $callable) { } if ($callable->expression->member instanceof Literal) { $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); + } else if ($callable->expression->member instanceof Expression) { + $result->out->write(','); + $this->emitOne($result, $callable->expression->member->inline); } else { $result->out->write(','); $this->emitOne($result, $callable->expression->member); diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index af43f354..08ca6da3 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -9,7 +9,7 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteExplicitOctals, RewriteEnums; use ReadonlyClasses, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; /** 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 607e1e68..5dfd32c4 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions, ReadonlyClasses; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses; /** 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 8a784cd9..b298a0e8 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -10,7 +10,7 @@ * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { - use RewriteBlockLambdaExpressions, ReadonlyClasses; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php index 9a897ea6..c1da8601 100755 --- a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php +++ b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php @@ -1,6 +1,6 @@ member instanceof Literal && 'class' === $scope->member->expression && !is_string($scope->type)) { @@ -16,7 +17,7 @@ protected function emitScope($result, $scope) { $this->emitOne($result, $scope->type); $result->out->write(')'); } else { - parent::emitScope($result, $scope); + $this->rewriteDynamicClassConstants($result, $scope); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php b/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php new file mode 100755 index 00000000..b71d3f0d --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php @@ -0,0 +1,21 @@ +}`. + * + * @see https://wiki.php.net/rfc/dynamic_class_constant_fetch + */ +trait RewriteDynamicClassConstants { + + protected function emitScope($result, $scope) { + if ($scope->member instanceof Expression && -1 !== $scope->line) { + $result->out->write('constant('.$scope->type.'::class."::".'); + $this->emitOne($result, $scope->member->inline); + $result->out->write(')'); + } else { + parent::emitScope($result, $scope); + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 23050040..d386d3b4 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -74,7 +74,10 @@ public function emit_node_without_kind() { #[Test] public function transform_modifying_node() { $fixture= $this->newEmitter(); - $fixture->transform('variable', function($codegen, $var) { $var->name= '_'.$var->name; return $var; }); + $fixture->transform('variable', function($codegen, $var) { + $var->pointer= '_'.$var->pointer; + return $var; + }); $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); Assert::equals('bytes()); @@ -83,7 +86,9 @@ public function transform_modifying_node() { #[Test] public function transform_to_node() { $fixture= $this->newEmitter(); - $fixture->transform('variable', function($codegen, $var) { return new Code('$variables["'.$var->name.'"]'); }); + $fixture->transform('variable', function($codegen, $var) { + return new Code('$variables["'.$var->pointer.'"]'); + }); $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); Assert::equals('bytes()); @@ -92,7 +97,9 @@ public function transform_to_node() { #[Test] public function transform_to_array() { $fixture= $this->newEmitter(); - $fixture->transform('variable', function($codegen, $var) { return [new Code('$variables["'.$var->name.'"]')]; }); + $fixture->transform('variable', function($codegen, $var) { + return [new Code('$variables["'.$var->pointer.'"]')]; + }); $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); Assert::equals('bytes()); diff --git a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php index f32c1cfc..f035a1a9 100755 --- a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php @@ -26,6 +26,30 @@ public function run() { Assert::instance(Date::class, $r); } + #[Test] + public function new_dynamic_var() { + $r= $this->run('class { + public function run() { + $class= \\util\\Date::class; + $var= "class"; + return new $$var(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function new_var_expr() { + $r= $this->run('class { + public function run() { + $class= \\util\\Date::class; + $var= "class"; + return new ${$var}(); + } + }'); + Assert::instance(Date::class, $r); + } + #[Test] public function new_expr() { $r= $this->run('class { diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 6db1e8d2..0bdc00e3 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -70,7 +70,7 @@ public function run() { Assert::equals('Test', $r); } - #[Test, Values(['$this->$member', '$this->{$member}'])] + #[Test, Values(['$this->$member', '$this->{$member}', '$this->{strtoupper($member)}'])] public function dynamic_instance_property($syntax) { $r= $this->run('class { private $MEMBER= "Test"; @@ -84,21 +84,21 @@ public function run() { Assert::equals('Test', $r); } - #[Test, Ignore('Unsupported!')] - public function dynamic_class_property() { + #[Test, Values(['self::$$member', 'self::${$member}', 'self::${strtoupper($member)}'])] + public function dynamic_class_property($syntax) { $r= $this->run('class { private static $MEMBER= "Test"; public function run() { $member= "MEMBER"; - return self::${$member}; + return '.$syntax.'; } }'); Assert::equals('Test', $r); } - #[Test, Values(['$this->$method()', '$this->{$method}()'])] + #[Test, Values(['$this->$method()', '$this->{$method}()', '$this->{strtolower($method)}()'])] public function dynamic_instance_method($syntax) { $r= $this->run('class { private function test() { return "Test"; } @@ -112,7 +112,7 @@ public function run() { Assert::equals('Test', $r); } - #[Test, Values(['self::$method()', 'self::{$method}()'])] + #[Test, Values(['self::$method()', 'self::{$method}()', 'self::{strtolower($method)}()'])] public function dynamic_class_method($syntax) { $r= $this->run('class { private static function test() { return "Test"; } @@ -126,14 +126,14 @@ public function run() { Assert::equals('Test', $r); } - #[Test, Ignore('Unsupported!')] - public function dynamic_class_constant() { + #[Test, Values(['self::{$member}', 'self::{strtoupper($member)}'])] + public function dynamic_class_constant($syntax) { $r= $this->run('class { const MEMBER= "Test"; public function run() { $member= "MEMBER"; - return self::{$member}; + return '.$syntax.'; } }'); From 24556f9e9a53ac7b710217c77046074d7c261a34 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 16:39:44 +0200 Subject: [PATCH 706/926] Try fixing version conflict See https://github.com/xp-framework/compiler/pull/161#issuecomment-1500904499 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5869b564..94443180 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": "dev-feature/expressions as 10.0.0", + "xp-framework/ast": "dev-feature/expressions as 9.99.0", "php" : ">=7.0.0" }, "require-dev" : { From 87242294b756e0627c932f54b095d1e33e48d2e7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 18:56:50 +0200 Subject: [PATCH 707/926] Use release version See https://github.com/xp-framework/ast/releases/tag/v10.0.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 94443180..1084a615 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": "dev-feature/expressions as 9.99.0", + "xp-framework/ast": "^10.0", "php" : ">=7.0.0" }, "require-dev" : { From effac73e7dd7575fbbdccd480d1c2da3c92d0f02 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 19:15:48 +0200 Subject: [PATCH 708/926] Add PHP 8.3 emitter --- ChangeLog.md | 6 +++ src/main/php/lang/ast/emit/PHP83.class.php | 45 ++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100755 src/main/php/lang/ast/emit/PHP83.class.php diff --git a/ChangeLog.md b/ChangeLog.md index a5bf79df..91a081fb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.10.0 / 2023-04-08 + +* Merged PR #161: Support dynamic class constant fetch, the first PHP + 8.3 feature: https://wiki.php.net/rfc/dynamic_class_constant_fetch + (@thekid) + ## 8.9.0 / 2023-02-19 * Merged PR #159: Implement command line option to set file extension diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php new file mode 100755 index 00000000..ea83477e --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -0,0 +1,45 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file From c856f006c714ff18808715b9a15603cea385b2f7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 19:29:10 +0200 Subject: [PATCH 709/926] Add PHP 8.3 emitter [skip ci] --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df2e76a3..666b4958 100755 --- a/README.md +++ b/README.md @@ -92,8 +92,9 @@ lang.ast.emit.PHP71 lang.ast.emit.PHP72 lang.ast.emit.PHP74 lang.ast.emit.PHP80 -lang.ast.emit.PHP81 [*] -lang.ast.emit.PHP82 +lang.ast.emit.PHP81 +lang.ast.emit.PHP82 [*] +lang.ast.emit.PHP83 lang.ast.syntax.php.Using [*] @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> From c2848e95f40fbab208bfa2b7dd0b725b6982d8af Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Apr 2023 19:33:33 +0200 Subject: [PATCH 710/926] Remove unused import --- src/main/php/lang/ast/emit/PHP80.class.php | 13 +++++++++++-- src/main/php/lang/ast/emit/PHP81.class.php | 13 +++++++++++-- src/main/php/lang/ast/emit/PHP82.class.php | 13 +++++++++++-- src/main/php/lang/ast/emit/PHP83.class.php | 13 +++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 08ca6da3..e4e13aa0 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,7 +1,16 @@ Date: Sun, 9 Apr 2023 10:43:07 +0200 Subject: [PATCH 711/926] Add tests for arbitrary static variable initializers See #162 --- .../ast/unittest/emit/StaticsTest.class.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/StaticsTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/StaticsTest.class.php b/src/test/php/lang/ast/unittest/emit/StaticsTest.class.php new file mode 100755 index 00000000..380f4ca8 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/StaticsTest.class.php @@ -0,0 +1,83 @@ +getMethod('run')->invoke($t->newInstance(), $args); + } + + #[Test] + public function constant_static() { + $t= $this->type('class { + public function run() { + static $i= 0; + + return $i++; + } + }'); + + Assert::equals(0, $this->apply($t)); + Assert::equals(1, $this->apply($t)); + Assert::equals(2, $this->apply($t)); + } + + #[Test] + public function initialization_to_new() { + $t= $this->type('use util\\{Date, Dates}; class { + public function run() { + static $t= new Date(0); + + return $t= Dates::add($t, 86400); + } + }'); + + Assert::equals(new Date(86400), $this->apply($t)); + Assert::equals(new Date(86400 * 2), $this->apply($t)); + } + + #[Test] + public function initialization_to_parameter() { + $t= $this->type('class { + public function run($initial) { + static $t= $initial; + + return $t; + } + }'); + + $instance= $t->newInstance(); + Assert::equals('initial', $this->apply($t, 'initial')); + Assert::equals('initial', $this->apply($t, 'changed')); + } + + #[Test] + public function initialization_when_throwing() { + $t= $this->type('use lang\\IllegalArgumentException; class { + public function run($initial) { + static $t= $initial ?? throw new IllegalArgumentException("May not be null"); + + return $t; + } + }'); + + // This does not initialize the static + Assert::throws(TargetInvocationException::class, function() use($t) { + $this->apply($t, null); + }); + + // This does + Assert::equals('initial', $this->apply($t, 'initial')); + } +} \ No newline at end of file From 322f64bd769bbd0f8c7bd60559e3e71f2d05d12e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Apr 2023 11:01:57 +0200 Subject: [PATCH 712/926] Add PHP 8.3 typed constants See https://wiki.php.net/rfc/typed_class_constants --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 666b4958..e71ddf7d 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack language, PHP 8.2, 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack language, PHP 8.3, PHP 8.2, 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; From 78f589110e893173fb6adfbfcca4d2a4b28fbf9a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 9 Apr 2023 18:14:33 +0200 Subject: [PATCH 713/926] Rename StaticsTest -> StaticLocalsTest --- .../emit/{StaticsTest.class.php => StaticLocalsTest.class.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/php/lang/ast/unittest/emit/{StaticsTest.class.php => StaticLocalsTest.class.php} (97%) diff --git a/src/test/php/lang/ast/unittest/emit/StaticsTest.class.php b/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php similarity index 97% rename from src/test/php/lang/ast/unittest/emit/StaticsTest.class.php rename to src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php index 380f4ca8..6346cecf 100755 --- a/src/test/php/lang/ast/unittest/emit/StaticsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php @@ -5,7 +5,7 @@ use util\Date; /** @see https://wiki.php.net/rfc/arbitrary_static_variable_initializers */ -class StaticsTest extends EmittingTest { +class StaticLocalsTest extends EmittingTest { /** * Calls the given type's `run()` method From 3bc3c9e42bc441125fd66d901eebb92f0d276012 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 10 Apr 2023 10:09:29 +0200 Subject: [PATCH 714/926] Emit meta data for class constants --- composer.json | 1 + src/main/php/lang/ast/emit/PHP.class.php | 9 +++++++++ .../lang/ast/unittest/emit/MembersTest.class.php | 14 ++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 1084a615..a6cc1bdd 100755 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "php" : ">=7.0.0" }, "require-dev" : { + "xp-framework/reflection": "feature/typed_class_constants as 2.11.0", "xp-framework/test": "^1.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 6842b785..49ffc37f 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -21,6 +21,7 @@ abstract class PHP extends Emitter { const PROPERTY = 0; const METHOD = 1; + const CONSTANT = 2; protected $literals= []; @@ -562,6 +563,14 @@ protected function emitUse($result, $use) { } protected function emitConst($result, $const) { + $result->codegen->scope[0]->meta[self::CONSTANT][$const->name]= [ + DETAIL_RETURNS => $const->type ? $const->type->name() : 'var', + DETAIL_ANNOTATIONS => $const->annotations, + DETAIL_COMMENT => $const->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + $const->comment && $this->emitOne($result, $const->comment); $const->annotations && $this->emitOne($result, $const->annotations); $result->at($const->declared)->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 0bdc00e3..b81e446f 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,6 +1,6 @@ run('class { + $t= Reflection::type($this->type('class { private const string MEMBER = "Test"; + }')); + $const= $t->constant('MEMBER'); - public function run() { - return self::MEMBER; - } - }'); - - Assert::equals('Test', $r); + Assert::equals('Test', $const->value()); + Assert::equals(Primitive::$STRING, $const->constraint()->type()); } #[Test, Values(['$this->$member', '$this->{$member}', '$this->{strtoupper($member)}'])] From 7c333d2631ed5bde388c3ad8f793511fca614a0c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 10 Apr 2023 10:11:21 +0200 Subject: [PATCH 715/926] Fix version constraint --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a6cc1bdd..270c52bd 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/reflection": "feature/typed_class_constants as 2.11.0", + "xp-framework/reflection": "dev-feature/typed_class_constants as 2.11.0", "xp-framework/test": "^1.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], From f0c83d0f3f4d51c921e7c6fad7ae9a7618b7e08e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 10 Apr 2023 10:17:35 +0200 Subject: [PATCH 716/926] Emit meta information in OmitConstModifiers::emitConst() --- src/main/php/lang/ast/emit/OmitConstModifiers.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php index 465590bc..cab1aacd 100755 --- a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php +++ b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php @@ -9,6 +9,14 @@ trait OmitConstModifiers { protected function emitConst($result, $const) { + $result->codegen->scope[0]->meta[self::CONSTANT][$const->name]= [ + DETAIL_RETURNS => $const->type ? $const->type->name() : 'var', + DETAIL_ANNOTATIONS => $const->annotations, + DETAIL_COMMENT => $const->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + $result->out->write('const '.$const->name.'='); $this->emitOne($result, $const->expression); $result->out->write(';'); From 32f2d10d4a64ca447a5205983d14653069c4b62d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 10 Apr 2023 10:22:33 +0200 Subject: [PATCH 717/926] Fix `Attempt to modify property "meta" on null` --- .../emit/OmitConstModifiersTest.class.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php b/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php index 7c6fedba..393c6ac1 100755 --- a/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php @@ -1,11 +1,12 @@ type= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); + } + #[Test] public function omits_type() { - Assert::equals( - 'const TEST="test";', - $this->emit(new Constant([], 'TEST', new IsLiteral('string'), new Literal('"test"'))) - ); + $const= new Constant([], 'TEST', new IsLiteral('string'), new Literal('"test"')); + Assert::equals('const TEST="test";', $this->emit($const, [$this->type])); } #[Test] public function omits_modifier() { - Assert::equals( - 'const TEST="test";', - $this->emit(new Constant(['private'], 'TEST', new IsLiteral('string'), new Literal('"test"'))) - ); + $const= new Constant(['private'], 'TEST', new IsLiteral('string'), new Literal('"test"')); + Assert::equals('const TEST="test";', $this->emit($const, [$this->type])); } } \ No newline at end of file From 693eb5cf6732b0e7439e8e01771118ef9428b472 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Apr 2023 20:25:28 +0200 Subject: [PATCH 718/926] Document meta data for class constant types See #157 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 91a081fb..9f50ed9a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #163: Emit meta data for class constants. This makes types + accessible via reflection and is part of #157. + (@thekid) + ## 8.10.0 / 2023-04-08 * Merged PR #161: Support dynamic class constant fetch, the first PHP From 8f09ead964f91dccecead4e1b2f4a83623c71063 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Apr 2023 20:47:55 +0200 Subject: [PATCH 719/926] Omit constant types for all PHP versions < 8.3 --- .../lang/ast/emit/OmitConstantTypes.class.php | 18 ++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 18 +++++++++++++++++- src/main/php/lang/ast/emit/PHP70.class.php | 1 + src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + src/main/php/lang/ast/emit/PHP73.class.php | 1 + src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 13 +++++++++++-- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- src/main/php/lang/ast/emit/PHP82.class.php | 2 +- 10 files changed, 53 insertions(+), 5 deletions(-) create mode 100755 src/main/php/lang/ast/emit/OmitConstantTypes.class.php diff --git a/src/main/php/lang/ast/emit/OmitConstantTypes.class.php b/src/main/php/lang/ast/emit/OmitConstantTypes.class.php new file mode 100755 index 00000000..7685d428 --- /dev/null +++ b/src/main/php/lang/ast/emit/OmitConstantTypes.class.php @@ -0,0 +1,18 @@ +literal()) { + return null; + } else { + return $this->literal($type); + } + } + /** * Enclose a node inside a closure * @@ -573,7 +589,7 @@ protected function emitConst($result, $const) { $const->comment && $this->emitOne($result, $const->comment); $const->annotations && $this->emitOne($result, $const->annotations); - $result->at($const->declared)->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); + $result->at($const->declared)->out->write(implode(' ', $const->modifiers).' const '.$this->constantType($const->type).' '.$const->name.'='); $this->emitOne($result, $const->expression); $result->out->write(';'); } diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index e2425630..84c55b61 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -19,6 +19,7 @@ class PHP70 extends PHP { NullsafeAsTernaries, OmitArgumentNames, OmitConstModifiers, + OmitConstantTypes, OmitPropertyTypes, ReadonlyClasses, ReadonlyProperties, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 1c416f31..53324abb 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -18,6 +18,7 @@ class PHP71 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + OmitConstantTypes, OmitPropertyTypes, ReadonlyProperties, ReadonlyClasses, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 769faff8..9e92eb91 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -18,6 +18,7 @@ class PHP72 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + OmitConstantTypes, OmitPropertyTypes, ReadonlyProperties, ReadonlyClasses, diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index d0e32976..820b6e99 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -19,6 +19,7 @@ class PHP73 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + OmitConstantTypes, OmitPropertyTypes, ReadonlyProperties, ReadonlyClasses, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index e54d5615..da2f0e64 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -17,6 +17,7 @@ class PHP74 extends PHP { NonCapturingCatchVariables, NullsafeAsTernaries, OmitArgumentNames, + OmitConstantTypes, ReadonlyClasses, ReadonlyProperties, RewriteBlockLambdaExpressions, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index e4e13aa0..5783ea5c 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -18,8 +18,17 @@ * @see https://wiki.php.net/rfc#php_80 */ class PHP80 extends PHP { - use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteExplicitOctals, RewriteEnums; - use ReadonlyClasses, ReadonlyProperties, CallablesAsClosures, ArrayUnpackUsingMerge; + use + ArrayUnpackUsingMerge, + CallablesAsClosures, + OmitConstantTypes, + ReadonlyClasses, + ReadonlyProperties, + RewriteBlockLambdaExpressions, + RewriteDynamicClassConstants, + RewriteEnums, + RewriteExplicitOctals + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index d0b3fe59..83b8cbde 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; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; /** 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 c99e4de9..be3ae3b6 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; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; /** Sets up type => literal mappings */ public function __construct() { From 3228658c97053855040f5da9ebaf23fa2dcf2580 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Apr 2023 20:53:16 +0200 Subject: [PATCH 720/926] Use release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 270c52bd..4330dcfb 100755 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/reflection": "dev-feature/typed_class_constants as 2.11.0", + "xp-framework/reflection": "^2.11", "xp-framework/test": "^1.0" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], From ce57c7c7755858580b325749076e7e02b10bfea0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 17 Apr 2023 20:57:16 +0200 Subject: [PATCH 721/926] Release 8.11.0 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 9f50ed9a..b64bb904 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.11.0 / 2023-04-17 + +* Merged PR #165: Omit constant types for all PHP versions < 8.3. This + is the second part of #157, and fully implements typed constants, + https://wiki.php.net/rfc/typed_class_constants + (@thekid) * Merged PR #163: Emit meta data for class constants. This makes types accessible via reflection and is part of #157. (@thekid) From 5233129c80f79e8ce31589feaf0a3c72268cf256 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 May 2023 19:26:44 +0200 Subject: [PATCH 722/926] Ensure a semicolon exists before the `break` statement --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 337a0319..b15f8bb1 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -446,7 +446,7 @@ protected function emitClass($result, $class) { foreach ($context->virtual as $name => $access) { $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[0]); - $result->out->write('break;'); + $result->out->write(';break;'); } isset($context->virtual[null]) || $result->out->write( 'default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING);' @@ -457,7 +457,7 @@ protected function emitClass($result, $class) { foreach ($context->virtual as $name => $access) { $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[1]); - $result->out->write('break;'); + $result->out->write(';break;'); } $result->out->write('}}'); } From f6d6a62d3cb11ff18a9c94f3cf77a002c723dd42 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 May 2023 21:55:03 +0200 Subject: [PATCH 723/926] 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 724/926] 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 725/926] 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 726/926] 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 727/926] 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 728/926] 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 729/926] 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 730/926] 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 731/926] 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 732/926] 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 62258cfbd4c068820093b6a5ba35cdd1608ea908 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 14:03:30 +0200 Subject: [PATCH 733/926] Use "Dots" output --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fdfe444..68a5dff3 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,4 +53,4 @@ jobs: echo "vendor/autoload.php" > composer.pth - name: Run test suite - run: sh xp-run xp.test.Runner src/test/php + run: sh xp-run xp.test.Runner -r Dots src/test/php From ba11512a8a8a0aef4a70c9e5a708626f4c20ee5f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 14:04:05 +0200 Subject: [PATCH 734/926] Use Assert::matches() See https://github.com/xp-framework/test/pull/23 --- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 598c8f9d..9f54af1b 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -120,7 +120,7 @@ public function load_class_bytes() { $code= $this->compile(['Tests' => 'loadClassBytes($types['Tests']); }); - Assert::true((bool)preg_match('/<\?php .+ class Tests/', $code)); + Assert::match('/<\?php .+ class Tests/', $code); } #[Test] From 21c2a24c264abb95f0f7fb2c572cbb4b86c1c52c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 14:06:15 +0200 Subject: [PATCH 735/926] Bump requirement for xp-framework/test --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4330dcfb..9cd2e5df 100755 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ }, "require-dev" : { "xp-framework/reflection": "^2.11", - "xp-framework/test": "^1.0" + "xp-framework/test": "^1.5" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { From 3b651af3a5896779fc9b1aeda9c242028ae1cbc8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 14:23:49 +0200 Subject: [PATCH 736/926] Fix assertion shorthand --- .../lang/ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 9f54af1b..14819c2c 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -120,7 +120,7 @@ public function load_class_bytes() { $code= $this->compile(['Tests' => 'loadClassBytes($types['Tests']); }); - Assert::match('/<\?php .+ class Tests/', $code); + Assert::matches('/<\?php .+ class Tests/', $code); } #[Test] From 9830d042f9b1a537c44ab8fa96f78ec194895752 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 15:30:15 +0200 Subject: [PATCH 737/926] 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 738/926] 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 739/926] 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 740/926] 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 fa5ab92b8beb839773273b93b3ee6daf5e16da69 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 20:43:05 +0200 Subject: [PATCH 741/926] Do not misuse Scope::$init for non-constant parameter defaults Scope::$init is strictly for member initializations --- src/main/php/lang/ast/emit/PHP.class.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b15f8bb1..f25aa8db 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -316,7 +316,6 @@ protected function emitParameter($result, $parameter) { $this->emitOne($result, $parameter->default); } else { $result->out->write('=null'); - $result->codegen->scope[0]->init['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default; } } $result->locals[$parameter->name]= true; @@ -634,13 +633,14 @@ protected function emitMethod($result, $method) { $method->annotations && $this->emitOne($result, $method->annotations); $result->at($method->declared)->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $promoted= []; + $promoted= $init= []; foreach ($method->signature->parameters as $param) { if (isset($param->promote)) $promoted[]= $param; // Create a parameter annotation named `default` for non-constant parameter defaults if (isset($param->default) && !$this->isConstant($result, $param->default)) { $param->annotate(new Annotation('default', [$param->default])); + $init[]= $param; } $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; @@ -652,11 +652,25 @@ protected function emitMethod($result, $method) { $result->out->write(';'); } else { $result->out->write(' {'); - $this->emitInitializations($result, $result->codegen->scope[0]->init); - $result->codegen->scope[0]->init= []; + + // Emit initializations if inside constructor + if ('__construct' === $method->name) { + $this->emitInitializations($result, $result->codegen->scope[0]->init); + $result->codegen->scope[0]->init= []; + } + + // Emit non-constant parameter defaults + foreach ($init as $param) { + $result->out->write('null === $'.$param->name.' && $'.$param->name.'='); + $this->emitOne($result, $param->default); + $result->out->write(';'); + } + + // Emit promoted parameters foreach ($promoted as $param) { $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } + $this->emitAll($result, $method->body); $result->out->write('}'); } From 4b1168a62f8e8a96858b11a497de94b99f171da6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 15:02:37 +0200 Subject: [PATCH 742/926] Give initializations access to constructor parameters --- src/main/php/lang/ast/emit/PHP.class.php | 12 ++++++------ .../unittest/emit/ArgumentPromotionTest.class.php | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f25aa8db..a13417c6 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -653,12 +653,6 @@ protected function emitMethod($result, $method) { } else { $result->out->write(' {'); - // Emit initializations if inside constructor - if ('__construct' === $method->name) { - $this->emitInitializations($result, $result->codegen->scope[0]->init); - $result->codegen->scope[0]->init= []; - } - // Emit non-constant parameter defaults foreach ($init as $param) { $result->out->write('null === $'.$param->name.' && $'.$param->name.'='); @@ -671,6 +665,12 @@ protected function emitMethod($result, $method) { $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); } + // Emit initializations if inside constructor + if ('__construct' === $method->name) { + $this->emitInitializations($result, $result->codegen->scope[0]->init); + $result->codegen->scope[0]->init= []; + } + $this->emitAll($result, $method->body); $result->out->write('}'); } diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index cbe3700b..b98924a6 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -127,4 +127,13 @@ public function __construct( ) { } }'); } + + #[Test] + public function initializations_have_access() { + $t= $this->type('class { + public $first= $this->list[0] ?? null; + public function __construct(private array $list) { } + }'); + Assert::equals('Test', $t->newInstance(['Test'])->first); + } } \ No newline at end of file From 9dede278f4faa2571a2bd6467f332b75cd2df655 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 15:57:24 +0200 Subject: [PATCH 743/926] Implement returning by reference from methods & functions --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 9 +++++++-- .../ast/unittest/emit/MembersTest.class.php | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 9cd2e5df..1d7bbdcf 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": "^10.1", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a13417c6..81d94301 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -338,7 +338,7 @@ protected function emitFunction($result, $function) { $result->stack[]= $result->locals; $result->locals= []; - $result->out->write('function '.$function->name); + $result->out->write('function '.($function->signature->byref ? '&' : '').$function->name); $this->emitSignature($result, $function->signature); $result->out->write('{'); @@ -631,7 +631,12 @@ protected function emitMethod($result, $method) { $method->comment && $this->emitOne($result, $method->comment); $method->annotations && $this->emitOne($result, $method->annotations); - $result->at($method->declared)->out->write(implode(' ', $method->modifiers).' function '.$method->name); + $result->at($method->declared)->out->write( + implode(' ', $method->modifiers). + ' function '. + ($method->signature->byref ? '&' : ''). + $method->name + ); $promoted= $init= []; foreach ($method->signature->parameters as $param) { diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index b81e446f..d05066dd 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -219,6 +219,23 @@ public function run() { Assert::equals([1, 2, 3], $r); } + #[Test] + public function return_by_reference() { + $r= $this->run('class { + private $list= []; + + public function &list() { return $this->list; } + + public function run() { + $list= &$this->list(); + $list[]= "Test"; + return $this->list; + } + + }'); + Assert::equals(['Test'], $r); + } + #[Test, Values(['variable', 'invocation', 'array'])] public function class_on_objects($via) { $t= $this->type('class { From d4156c520d3c3805bc9fc6db9f401dd60bcf22eb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:01:14 +0200 Subject: [PATCH 744/926] Document initializations' access to constructor parameters --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index b64bb904..fe0a0a64 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Make it possible to refer to constructor parameters inside property + initialization expressions. + (@thekid) + ## 8.11.0 / 2023-04-17 * Merged PR #165: Omit constant types for all PHP versions < 8.3. This From 9547a2ef6082e6d8d37d51066e43b17704fcb1f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:03:02 +0200 Subject: [PATCH 745/926] Release 8.12.0 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index fe0a0a64..21f504a1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.12.0 / 2023-05-21 + +* Merged PR #168: Return by reference from methods and functions, see + https://www.php.net/manual/en/language.references.return.php + (@thekid) * Make it possible to refer to constructor parameters inside property initialization expressions. (@thekid) From c32235c2517e97af827cf2c15e441c8df9076358 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:07:18 +0200 Subject: [PATCH 746/926] 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 747/926] 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 748/926] 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 a8d59353c6c670678243ba71b3ddfe80468b1731 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 27 May 2023 19:38:41 +0200 Subject: [PATCH 749/926] Rewrite to use new reflection API --- .../ast/unittest/emit/AnonymousClassTest.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 5a918da4..e6059fa8 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -1,6 +1,6 @@ null], typeof($r)->getMethod('fixture')->getAnnotations()); + Assert::equals([], Reflection::type($r)->method('fixture')->annotation('Inside')->arguments()); } #[Test] @@ -74,20 +74,20 @@ public static function run() { return new class() extends self { }; } }'); - Assert::instance($t, $t->getMethod('run')->invoke(null)); + Assert::instance($t, Reflection::type($t)->method('run')->invoke(null)); } #[Test] public function referencing_enclosing_class() { - $t= $this->type('class { - const ID = 6100; + $r= $this->run('class { + const ID= 6100; public static function run() { return new class() extends self { - public static $id = ::ID; + public static $id= ::ID; }; } }'); - Assert::instance($t, $t->getMethod('run')->invoke(null)); + Assert::equals(6100, Reflection::type($r)->property('id')->get(null)); } } \ No newline at end of file From 3a51fe684dbf0b8585280ba77f1ada8e55d70966 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 00:44:02 +0200 Subject: [PATCH 750/926] Fix ambiguity resolution for parameter annotations --- src/main/php/lang/ast/emit/php/XpMeta.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php index 18e514d9..728bfd16 100755 --- a/src/main/php/lang/ast/emit/php/XpMeta.class.php +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -17,7 +17,7 @@ trait XpMeta { /** Stores lowercased, unnamespaced name in annotations for BC reasons! */ - protected function annotations($result, $annotations) { + protected function annotations($result, $annotations, $resolve= null) { if (null === $annotations) return []; $lookup= []; @@ -32,7 +32,7 @@ protected function annotations($result, $annotations) { } else if (1 === sizeof($arguments) && isset($arguments[0])) { $this->emitOne($result, $arguments[0]); $result->out->write(','); - $lookup[$name]= 1; // Resolve ambiguity + $lookup[$name][$resolve]= 1; // Resolve ambiguity } else { $result->out->write('['); foreach ($arguments as $name => $argument) { @@ -53,13 +53,13 @@ private function attributes($result, $annotations, $target) { $result->out->write('], DETAIL_TARGET_ANNO => ['); foreach ($target as $name => $annotations) { $result->out->write("'$".$name."' => ["); - foreach ($this->annotations($result, $annotations) as $key => $value) { + foreach ($this->annotations($result, $annotations, $name) as $key => $value) { $lookup[$key]= $value; } $result->out->write('],'); } foreach ($lookup as $key => $value) { - $result->out->write("'".$key."' => '".$value."',"); + $result->out->write("'{$key}' => ".var_export($value, true).','); } $result->out->write(']'); } From 456927cfc04bc61a862d857a23982a450b278878 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 09:30:05 +0200 Subject: [PATCH 751/926] Refactor how annotations are emitted --- composer.json | 4 +- .../ast/emit/AttributesAsComments.class.php | 21 +++ src/main/php/lang/ast/emit/PHP.class.php | 28 ++-- .../unittest/emit/AnnotationSupport.class.php | 152 ++++++++++++------ 4 files changed, 137 insertions(+), 68 deletions(-) diff --git a/composer.json b/composer.json index 1d7bbdcf..a23d7a6b 100755 --- a/composer.json +++ b/composer.json @@ -6,12 +6,12 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^11.0 | ^10.0", + "xp-framework/core": "^11.6", "xp-framework/ast": "^10.1", "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/reflection": "^2.11", + "xp-framework/reflection": "^2.13", "xp-framework/test": "^1.5" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 8870790c..09929b96 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -16,6 +16,27 @@ protected function emitAnnotation($result, $annotation) { $result->out->write('\\'.$annotation->name); if (empty($annotation->arguments)) return; + // Check whether arguments are constant, enclose in `eval` array + // otherwise. This is not strictly necessary but will ensure + // forward compatibility with PHP 8 + foreach ($annotation->arguments as $argument) { + if ($this->isConstant($result, $argument)) continue; + + $escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); + $result->out->write('(eval: ['); + foreach ($annotation->arguments as $name => $argument) { + is_string($name) && $result->out->write("'{$name}'=>"); + + $result->out->write("'"); + $result->out= $escaping; + $this->emitOne($result, $argument); + $result->out= $escaping->original(); + $result->out->write("',"); + } + $result->out->write('])'); + return; + } + // We can use named arguments here as PHP 8 attributes are parsed // by the XP reflection API when using PHP 7. However, we may not // emit trailing commas here! diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 81d94301..26ad1bef 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -495,24 +495,18 @@ protected function emitAnnotation($result, $annotation) { if ($this->isConstant($result, $argument)) continue; // Found first non-constant argument, enclose in `eval` - $result->out->write('(eval: \''); - $result->out= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); - - // If exactly one unnamed argument exists, emit its value directly - if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { - $this->emitOne($result, current($annotation->arguments)); - } else { - $result->out->write('['); - foreach ($annotation->arguments as $key => $argument) { - $result->out->write("'{$key}'=>"); - $this->emitOne($result, $argument); - $result->out->write(','); - } - $result->out->write(']'); + $escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); + $result->out->write('(eval: ['); + foreach ($annotation->arguments as $name => $argument) { + is_string($name) && $result->out->write("'{$name}'=>"); + + $result->out->write("'"); + $result->out= $escaping; + $this->emitOne($result, $argument); + $result->out= $escaping->original(); + $result->out->write("',"); } - - $result->out= $result->out->original(); - $result->out->write('\')'); + $result->out->write('])'); return; } diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index 8bd2e6c6..11295dd6 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -1,6 +1,6 @@ type( + $declaration.(strstr($declaration, '') ? '' : ' class { }') + )); + } + + /** + * Returns annotations present in the given type + * + * @param lang.reflection.Annotated $annotated + * @return [:var[]] + */ + private function annotations($annotated) { + $r= []; + foreach ($annotated->annotations() as $name => $annotation) { + $r[$name]= $annotation->arguments(); + } + return $r; + } + #[Test] public function without_value() { - $t= $this->type('#[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); + Assert::equals( + ['Test' => []], + $this->annotations($this->declare('#[Test]')) + ); } #[Test] public function within_namespace() { - $t= $this->type('namespace tests; #[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); + Assert::equals( + ['tests\\Test' => []], + $this->annotations($this->declare('namespace tests; #[Test]')) + ); } #[Test] public function resolved_against_import() { - $t= $this->type('use unittest\Test; #[Test] class { }'); - Assert::equals(['test' => null], $t->getAnnotations()); + Assert::equals( + ['unittest\\Test' => []], + $this->annotations($this->declare('use unittest\Test; #[Test]')) + ); } #[Test] public function primitive_value() { - $t= $this->type('#[Author("Timm")] class { }'); - Assert::equals(['author' => 'Timm'], $t->getAnnotations()); + Assert::equals( + ['Author' => ['Timm']], + $this->annotations($this->declare('#[Author("Timm")]')) + ); } #[Test] public function array_value() { - $t= $this->type('#[Authors(["Timm", "Alex"])] class { }'); - Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); + Assert::equals( + ['Authors' => [['Timm', 'Alex']]], + $this->annotations($this->declare('#[Authors(["Timm", "Alex"])]')) + ); } #[Test] public function map_value() { - $t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class { }'); - Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + Assert::equals( + ['Expect' => [['class' => IllegalArgumentException::class]]], + $this->annotations($this->declare('#[Expect(["class" => \lang\IllegalArgumentException::class])]')) + ); } #[Test] public function named_argument() { - $t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class { }'); - Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); + Assert::equals( + ['Expect' => ['class' => IllegalArgumentException::class]], + $this->annotations($this->declare('#[Expect(class: \lang\IllegalArgumentException::class)]')) + ); } #[Test] public function closure_value() { - $t= $this->type('#[Verify(function($arg) { return $arg; })] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f('test')); + $verify= $this->annotations($this->declare('#[Verify(function($arg) { return $arg; })]'))['Verify']; + Assert::equals('test', $verify[0]('test')); } #[Test] public function arrow_function_value() { - $t= $this->type('#[Verify(fn($arg) => $arg)] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f('test')); + $verify= $this->annotations($this->declare('#[Verify(fn($arg) => $arg)]'))['Verify']; + Assert::equals('test', $verify[0]('test')); } #[Test] public function array_of_arrow_function_value() { - $t= $this->type('#[Verify([fn($arg) => $arg])] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f[0]('test')); + $verify= $this->annotations($this->declare('#[Verify([fn($arg) => $arg])]'))['Verify']; + Assert::equals('test', $verify[0][0]('test')); } #[Test] public function named_arrow_function_value() { - $t= $this->type('#[Verify(func: fn($arg) => $arg)] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('test', $f['func']('test')); + $verify= $this->annotations($this->declare('#[Verify(func: fn($arg) => $arg)]'))['Verify']; + Assert::equals('test', $verify['func']('test')); } #[Test] public function single_quoted_string_inside_non_constant_expression() { - $t= $this->type('#[Verify(fn($arg) => \'php\\\\\'.$arg)] class { }'); - $f= $t->getAnnotation('verify'); - Assert::equals('php\\test', $f('test')); + $verify= $this->annotations($this->declare('#[Verify(fn($arg) => \'php\\\\\'.$arg)]'))['Verify']; + Assert::equals('php\\test', $verify[0]('test')); } #[Test] public function has_access_to_class() { - $t= $this->type('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }'); - Assert::equals(['expect' => true], $t->getAnnotations()); + Assert::equals( + ['Expect' => [true]], + $this->annotations($this->declare('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }')) + ); } #[Test] public function method() { - $t= $this->type('class { #[Test] public function fixture() { } }'); - Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations()); + $t= $this->declare('class { #[Test] public function fixture() { } }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->method('fixture')) + ); } #[Test] public function field() { - $t= $this->type('class { #[Test] public $fixture; }'); - Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations()); + $t= $this->declare('class { #[Test] public $fixture; }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->property('fixture')) + ); } #[Test] public function param() { - $t= $this->type('class { public function fixture(#[Test] $param) { } }'); - Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); + $t= $this->declare('class { public function fixture(#[Test] $param) { } }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->method('fixture')->parameter(0)) + ); } #[Test] public function params() { - $t= $this->type('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); - $m= $t->getMethod('fixture'); + $t= $this->declare('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); Assert::equals( - [['inject' => ['name' => 'a']], ['inject' => null]], - [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] + ['Inject' => [['name' => 'a']]], + $this->annotations($t->method('fixture')->parameter(0)) + ); + Assert::equals( + ['Inject' => []], + $this->annotations($t->method('fixture')->parameter(1)) ); } #[Test] public function multiple_class_annotations() { - $t= $this->type('#[Resource("/"), Authenticated] class { }'); - Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations()); + Assert::equals( + ['Resource' => ['/'], 'Authenticated' => []], + $this->annotations($this->declare('#[Resource("/"), Authenticated]')) + ); } #[Test] public function multiple_member_annotations() { - $t= $this->type('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); - Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations()); + $t= $this->declare('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); + Assert::equals( + ['Test' => [], 'Values' => [[1, 2, 3]]], + $this->annotations($t->method('fixture')) + ); } #[Test] public function multiline_annotations() { - $t= $this->type(' + $annotations= $this->annotations($this->declare(' #[Authors([ "Timm", "Mr. Midori", ])] class { }' - ); - Assert::equals(['authors' => ['Timm', 'Mr. Midori']], $t->getAnnotations()); + )); + Assert::equals(['Authors' => [['Timm', 'Mr. Midori']]], $annotations); } } \ No newline at end of file From ef3a17115bc3feab8f69ab8141ef10b6130a0e10 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 10:47:36 +0200 Subject: [PATCH 752/926] Restore compatibility with XP 10 See https://github.com/xp-framework/compiler/pull/169#discussion_r1216410017 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a23d7a6b..56dcd208 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^11.6", + "xp-framework/core": "^11.6 | ^10.16", "xp-framework/ast": "^10.1", "php" : ">=7.0.0" }, From b980f30e955733f89c0dddfecd332a455318fba7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 10:56:53 +0200 Subject: [PATCH 753/926] Release 8.13.0 --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 21f504a1..2e1e3ec5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.13.0 / 2023-06-04 + +* Merged PR #169: Refactor how annotations with non-constant arguments + are emitted. This enables a transition to `xp-framework/reflection` + as proposed in xp-framework/rfc#338 + (@thekid) + ## 8.12.0 / 2023-05-21 * Merged PR #168: Return by reference from methods and functions, see From b635068570393a2dc90b946994e222c0a0b3399f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 11:32:57 +0200 Subject: [PATCH 754/926] Move declare() to parent class See https://github.com/xp-framework/compiler/pull/169#discussion_r1216415649 --- .../unittest/emit/AnnotationSupport.class.php | 28 ++++---------- .../ast/unittest/emit/EmittingTest.class.php | 37 ++++++++++++++++++- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index 11295dd6..b46612e4 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -1,6 +1,6 @@ type( - $declaration.(strstr($declaration, '') ? '' : ' class { }') - )); - } - /** * Returns annotations present in the given type * @@ -127,13 +115,13 @@ public function single_quoted_string_inside_non_constant_expression() { public function has_access_to_class() { Assert::equals( ['Expect' => [true]], - $this->annotations($this->declare('#[Expect(self::SUCCESS)] class { const SUCCESS = true; }')) + $this->annotations($this->declare('#[Expect(self::SUCCESS)] class %T { const SUCCESS = true; }')) ); } #[Test] public function method() { - $t= $this->declare('class { #[Test] public function fixture() { } }'); + $t= $this->declare('class %T { #[Test] public function fixture() { } }'); Assert::equals( ['Test' => []], $this->annotations($t->method('fixture')) @@ -142,7 +130,7 @@ public function method() { #[Test] public function field() { - $t= $this->declare('class { #[Test] public $fixture; }'); + $t= $this->declare('class %T { #[Test] public $fixture; }'); Assert::equals( ['Test' => []], $this->annotations($t->property('fixture')) @@ -151,7 +139,7 @@ public function field() { #[Test] public function param() { - $t= $this->declare('class { public function fixture(#[Test] $param) { } }'); + $t= $this->declare('class %T { public function fixture(#[Test] $param) { } }'); Assert::equals( ['Test' => []], $this->annotations($t->method('fixture')->parameter(0)) @@ -160,7 +148,7 @@ public function param() { #[Test] public function params() { - $t= $this->declare('class { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); + $t= $this->declare('class %T { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); Assert::equals( ['Inject' => [['name' => 'a']]], $this->annotations($t->method('fixture')->parameter(0)) @@ -181,7 +169,7 @@ public function multiple_class_annotations() { #[Test] public function multiple_member_annotations() { - $t= $this->declare('class { #[Test, Values([1, 2, 3])] public function fixture() { } }'); + $t= $this->declare('class %T { #[Test, Values([1, 2, 3])] public function fixture() { } }'); Assert::equals( ['Test' => [], 'Values' => [[1, 2, 3]]], $this->annotations($t->method('fixture')) @@ -195,7 +183,7 @@ public function multiline_annotations() { "Timm", "Mr. Midori", ])] - class { }' + class %T { }' )); Assert::equals(['Authors' => [['Timm', 'Mr. Midori']]], $annotations); } diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 1f107045..0b06ae8d 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -1,10 +1,10 @@ cl->loadClass($class); } + /** + * Declare a type with a unique type name (which may be referenced by `%T`) + * and return a reflection instance referencing it. + * + * @param string $code + * @return lang.reflection.Type + */ + protected function declare($code) { + $name= 'T'.(self::$id++); + $declaration= strstr($code, '%T') + ? str_replace('%T', $name, $code) + : $code.' class '.$name.' { }' + ; + + $tree= $this->language->parse(new Tokens($declaration, static::class))->tree(); + if (isset($this->output['ast'])) { + Console::writeLine(); + Console::writeLine('=== ', static::class, ' ==='); + Console::writeLine($tree); + } + + $out= new MemoryOutputStream(); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); + if (isset($this->output['code'])) { + Console::writeLine(); + Console::writeLine('=== ', static::class, ' ==='); + Console::writeLine($out->bytes()); + } + + $class= ($package= $tree->scope()->package) ? strtr(substr($package, 1), '\\', '.').'.'.$name : $name; + $this->cl->setClassBytes($class, $out->bytes()); + return Reflection::type($this->cl->loadClass0($class)); + } + /** * Run code * From ae583cc02ae11343d689cf869409e301f42f9ab4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 13:04:14 +0200 Subject: [PATCH 755/926] Migrate type() -> declare() using new reflection API --- .../emit/AnonymousClassTest.class.php | 16 ++-- .../emit/ArgumentPromotionTest.class.php | 32 ++++--- .../emit/ArgumentUnpackingTest.class.php | 12 +-- .../unittest/emit/ArrayTypesTest.class.php | 16 ++-- .../ast/unittest/emit/ArraysTest.class.php | 36 ++++---- .../ast/unittest/emit/BlockTest.class.php | 4 +- .../ast/unittest/emit/BracesTest.class.php | 18 ++-- .../emit/CallableSyntaxTest.class.php | 34 +++---- .../ast/unittest/emit/CastingTest.class.php | 26 +++--- .../unittest/emit/ClassLiteralTest.class.php | 18 ++-- .../ast/unittest/emit/CommentsTest.class.php | 36 ++++---- .../emit/ControlStructuresTest.class.php | 26 +++--- .../ast/unittest/emit/DeclareTest.class.php | 6 +- .../lang/ast/unittest/emit/EchoTest.class.php | 2 +- .../ast/unittest/emit/EmittingTest.class.php | 31 +------ .../lang/ast/unittest/emit/EnumTest.class.php | 90 +++++++++++-------- .../unittest/emit/ExceptionsTest.class.php | 34 +++---- .../unittest/emit/FunctionTypesTest.class.php | 8 +- .../lang/ast/unittest/emit/GotoTest.class.php | 4 +- .../ast/unittest/emit/ImportTest.class.php | 12 +-- .../InitializeWithExpressionsTest.class.php | 44 ++++----- .../unittest/emit/InstanceOfTest.class.php | 16 ++-- .../unittest/emit/InstantiationTest.class.php | 12 +-- .../emit/IntersectionTypesTest.class.php | 28 +++--- .../unittest/emit/InvocationTest.class.php | 22 ++--- .../ast/unittest/emit/LambdasTest.class.php | 36 ++++---- .../ast/unittest/emit/LoopsTest.class.php | 24 ++--- .../ast/unittest/emit/MembersTest.class.php | 80 ++++++++--------- .../unittest/emit/MultipleCatchTest.class.php | 2 +- .../unittest/emit/NamespacesTest.class.php | 18 ++-- .../emit/NullCoalesceAssignmentTest.class.php | 4 +- .../unittest/emit/NullCoalesceTest.class.php | 6 +- .../ast/unittest/emit/NullSafeTest.class.php | 22 ++--- .../ast/unittest/emit/ParameterTest.class.php | 87 +++++++++--------- .../unittest/emit/PrecedenceTest.class.php | 10 +-- .../unittest/emit/PropertyTypesTest.class.php | 13 +-- .../ast/unittest/emit/ReadonlyTest.class.php | 58 ++++++------ .../ast/unittest/emit/ReturnTest.class.php | 6 +- .../ast/unittest/emit/ScalarsTest.class.php | 14 +-- .../unittest/emit/StaticLocalsTest.class.php | 14 +-- .../ast/unittest/emit/TernaryTest.class.php | 10 +-- .../emit/TrailingCommasTest.class.php | 16 ++-- .../ast/unittest/emit/TraitsTest.class.php | 32 +++---- .../emit/TransformationsTest.class.php | 24 ++--- .../emit/TypeDeclarationTest.class.php | 72 ++++++++------- .../emit/UnicodeEscapesTest.class.php | 4 +- .../unittest/emit/UnionTypesTest.class.php | 36 ++++---- .../ast/unittest/emit/UsingTest.class.php | 14 +-- .../ast/unittest/emit/VarargsTest.class.php | 4 +- .../emit/VirtualPropertyTypesTest.class.php | 36 ++++---- .../ast/unittest/emit/YieldTest.class.php | 14 +-- 51 files changed, 624 insertions(+), 615 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index e6059fa8..6c769eb6 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -14,7 +14,7 @@ class AnonymousClassTest extends EmittingTest { #[Test] public function parentless() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new class() { public function id() { return "test"; } @@ -26,7 +26,7 @@ public function id() { return "test"; } #[Test] public function extending_base_class() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new class() extends \\util\\AbstractDeferredInvokationHandler { public function initialize() { @@ -40,7 +40,7 @@ public function initialize() { #[Test] public function implementing_interface() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new class() implements \\lang\\Runnable { public function run() { @@ -54,7 +54,7 @@ public function run() { #[Test] public function method_annotations() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new class() { @@ -69,22 +69,22 @@ public function fixture() { } #[Test] public function extending_enclosing_class() { - $t= $this->type('class { + $t= $this->declare('class %T { public static function run() { return new class() extends self { }; } }'); - Assert::instance($t, Reflection::type($t)->method('run')->invoke(null)); + Assert::instance($t->class(), $t->method('run')->invoke(null)); } #[Test] public function referencing_enclosing_class() { - $r= $this->run('class { + $r= $this->run('class %T { const ID= 6100; public static function run() { return new class() extends self { - public static $id= ::ID; + public static $id= %T::ID; }; } }'); diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index b98924a6..1464e3f8 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -16,7 +16,7 @@ class ArgumentPromotionTest extends EmittingTest { #[Test] public function in_constructor() { - $r= $this->run('class { + $r= $this->run('class %T { public function __construct(private $id= "test") { // Empty } @@ -30,7 +30,7 @@ public function run() { #[Test] public function can_be_used_in_constructor() { - $r= $this->run('class { + $r= $this->run('class %T { public function __construct(private $id= "test") { $this->id.= "ed"; } @@ -44,7 +44,7 @@ public function run() { #[Test] public function parameter_accessible() { - $r= $this->run('class { + $r= $this->run('class %T { public function __construct(private $id= "test") { if (null === $id) { throw new \\lang\\IllegalArgumentException("ID not set"); @@ -60,7 +60,7 @@ public function run() { #[Test] public function in_method() { - $r= $this->run('class { + $r= $this->run('class %T { public function withId(private $id) { return $this; } @@ -74,36 +74,42 @@ public function run() { #[Test] public function type_information() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(private int $id, private string $name) { } }'); Assert::equals( [Primitive::$INT, Primitive::$STRING], - [$t->getField('id')->getType(), $t->getField('name')->getType()] + [$t->property('id')->constraint()->type(), $t->property('name')->constraint()->type()] ); } #[Test, Expect(class: Errors::class, message: '/Variadic parameters cannot be promoted/')] public function variadic_parameters_cannot_be_promoted() { - $this->type('class { + $this->declare('class %T { public function __construct(private string... $in) { } }'); } #[Test] public function can_be_mixed_with_normal_arguments() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(public string $name, string $initial= null) { if (null !== $initial) $this->name.= " ".$initial."."; } }'); - Assert::equals(['name'], array_map(function($f) { return $f->getName(); }, $t->getFields())); + + $names= []; + foreach ($t->properties() as $property) { + $names[]= $property->name(); + } + + Assert::equals(['name'], $names); Assert::equals('Timm J.', $t->newInstance('Timm', 'J')->name); } #[Test] public function promoted_by_reference_argument() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(public array &$list) { } public static function test() { @@ -114,12 +120,12 @@ public static function test() { } }'); - Assert::equals([1, 2, 3, 4], $t->getMethod('test')->invoke(null, [])); + Assert::equals([1, 2, 3, 4], $t->method('test')->invoke(null, [])); } #[Test] public function allows_trailing_comma() { - $this->type('class { + $this->declare('class %T { public function __construct( public float $x = 0.0, public float $y = 0.0, @@ -130,7 +136,7 @@ public function __construct( #[Test] public function initializations_have_access() { - $t= $this->type('class { + $t= $this->declare('class %T { public $first= $this->list[0] ?? null; public function __construct(private array $list) { } }'); diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index 61dd3b98..612cf26c 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -12,7 +12,7 @@ class ArgumentUnpackingTest extends EmittingTest { #[Test] public function invoking_method() { - $r= $this->run('class { + $r= $this->run('class %T { public function fixture(... $args) { return $args; } public function run() { @@ -25,7 +25,7 @@ public function run() { #[Test] public function in_array_initialization_with_variable() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $args= [3, 4]; return [1, 2, ...$args]; @@ -36,7 +36,7 @@ public function run() { #[Test] public function in_array_initialization_with_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return [1, 2, ...[3, 4]]; } @@ -46,7 +46,7 @@ public function run() { #[Test] public function in_map_initialization() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $args= ["type" => "car"]; return ["color" => "red", ...$args, "year" => 2002]; @@ -57,7 +57,7 @@ public function run() { #[Test] public function from_generator() { - $r= $this->run('class { + $r= $this->run('class %T { private function items() { yield 1; yield 2; @@ -72,7 +72,7 @@ public function run() { #[Test] public function from_iterator() { - $r= $this->run('class { + $r= $this->run('class %T { private function items() { return new \ArrayIterator([1, 2]); } diff --git a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php index f5d0d29f..4cfd15db 100755 --- a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php @@ -11,37 +11,37 @@ class ArrayTypesTest extends EmittingTest { #[Test] public function int_array_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array $test; }'); - Assert::equals('int[]', $t->getField('test')->getType()->getName()); + Assert::equals('int[]', $t->property('test')->constraint()->type()->getName()); } #[Test] public function int_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array $test; }'); - Assert::equals('[:int]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int]', $t->property('test')->constraint()->type()->getName()); } #[Test] public function nested_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array> $test; }'); - Assert::equals('[:int[]]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int[]]', $t->property('test')->constraint()->type()->getName()); } #[Test] public function var_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array $test; }'); - Assert::equals('[:var]', $t->getField('test')->getType()->getName()); + Assert::equals('[:var]', $t->property('test')->constraint()->type()->getName()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index f128cdde..dd95c2aa 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -7,7 +7,7 @@ class ArraysTest extends EmittingTest { #[Test] public function array_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return [1, 2, 3]; } @@ -18,7 +18,7 @@ public function run() { #[Test] public function map_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return ["a" => 1, "b" => 2]; } @@ -29,7 +29,7 @@ public function run() { #[Test] public function append() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $r= [1, 2]; $r[]= 3; @@ -42,7 +42,7 @@ public function run() { #[Test, Values(['[1, , 3]', '[1, , ]', '[, 1]']), Expect(class: IllegalStateException::class, message: 'Cannot use empty array elements in arrays')] public function arrays_cannot_have_empty_elements($input) { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return '.$input.'; } @@ -51,7 +51,7 @@ public function run() { #[Test] public function destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [$a, $b]= [1, 2]; return [$a, $b]; @@ -63,7 +63,7 @@ public function run() { #[Test] public function nested_destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [[$a, $b], $c]= [[1, 2], 3]; return [$a, $b, $c]; @@ -75,7 +75,7 @@ public function run() { #[Test] public function destructuring_with_empty_between() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [$a, , $b]= [1, 2, 3]; return [$a, $b]; @@ -87,7 +87,7 @@ public function run() { #[Test] public function destructuring_with_empty_start() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [, $a, $b]= [1, 2, 3]; return [$a, $b]; @@ -99,7 +99,7 @@ public function run() { #[Test] public function destructuring_map() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { ["two" => $a, "one" => $b]= ["one" => 1, "two" => 2]; return [$a, $b]; @@ -111,7 +111,7 @@ public function run() { #[Test] public function destructuring_map_with_expression() { - $r= $this->run('class { + $r= $this->run('class %T { private $one= "one"; public function run() { $two= "two"; @@ -125,7 +125,7 @@ public function run() { #[Test] public function nested_destructuring_with_map() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { ["nested" => ["one" => $a], "three" => $b]= ["nested" => ["one" => 1], "three" => 3]; return [$a, $b]; @@ -137,7 +137,7 @@ public function run() { #[Test, Values(['$list', '$this->instance', 'self::$static'])] public function reference_destructuring($reference) { - $r= $this->run('class { + $r= $this->run('class %T { private $instance= [1, 2]; private static $static= [1, 2]; @@ -155,7 +155,7 @@ public function run() { #[Test] public function list_destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { list($a, $b)= [1, 2]; return [$a, $b]; @@ -167,7 +167,7 @@ public function run() { #[Test] public function swap_using_destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $a= 1; $b= 2; @@ -181,7 +181,7 @@ public function run() { #[Test] public function result_of_destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return [$a, $b]= [1, 2]; } @@ -192,7 +192,7 @@ public function run() { #[Test, Values([null, true, false, 0, 0.5, '', 'Test'])] public function destructuring_with_non_array($value) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { $r= [$a, $b]= $arg; return [$a, $b, $r]; @@ -204,7 +204,7 @@ public function run($arg) { #[Test] public function init_with_variable() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $KEY= "key"; return [$KEY => "value"]; @@ -216,7 +216,7 @@ public function run() { #[Test] public function init_with_member_variable() { - $r= $this->run('class { + $r= $this->run('class %T { private static $KEY= "key"; public function run() { return [self::$KEY => "value"]; diff --git a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php index d5eeee37..833b76cd 100755 --- a/src/test/php/lang/ast/unittest/emit/BlockTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BlockTest.class.php @@ -6,7 +6,7 @@ class BlockTest extends EmittingTest { #[Test] public function empty_block() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { { } @@ -19,7 +19,7 @@ public function run() { #[Test] public function block_with_assignment() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { { $result= true; diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index a3b6bc2f..c9e4c128 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -6,7 +6,7 @@ class BracesTest extends EmittingTest { #[Test] public function inc() { - $r= $this->run('class { + $r= $this->run('class %T { private $id= 0; public function run() { @@ -19,7 +19,7 @@ public function run() { #[Test] public function braces_around_new() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return (new \\util\\Date(250905600))->getTime(); } @@ -30,7 +30,7 @@ public function run() { #[Test] public function no_braces_necessary_around_new() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new \\util\\Date(250905600)->getTime(); } @@ -41,7 +41,7 @@ public function run() { #[Test] public function property_vs_method_ambiguity() { - $r= $this->run('class { + $r= $this->run('class %T { private $f; public function __construct() { @@ -58,7 +58,7 @@ public function run() { #[Test] public function nested_braces() { - $r= $this->run('class { + $r= $this->run('class %T { private function test() { return "test"; } public function run() { @@ -71,7 +71,7 @@ public function run() { #[Test] public function braced_expression_not_confused_with_cast() { - $r= $this->run('class { + $r= $this->run('class %T { const WIDTH = 640; public function run() { @@ -84,7 +84,7 @@ public function run() { #[Test, Values([['(__LINE__)."test"', '3test'], ['(__LINE__) + 1', 4], ['(__LINE__) - 1', 2]])] public function global_constant_in_braces_not_confused_with_cast($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return '.$input.'; } @@ -95,7 +95,7 @@ public function run() { #[Test] public function function_call_in_braces() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $e= STDOUT; return false !== (fstat(STDOUT)); @@ -107,7 +107,7 @@ public function run() { #[Test] public function invoke_on_braced_null_coalesce() { - $r= $this->run('class { + $r= $this->run('class %T { public function __invoke() { return "OK"; } public function fail() { return function() { return "FAIL"; }; } diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 63eba40a..e9211bb9 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -23,14 +23,14 @@ private function verify($code) { #[Test] public function native_function() { - $this->verify('class { + $this->verify('class %T { public function run() { return strlen(...); } }'); } #[Test] public function instance_method() { - $this->verify('class { + $this->verify('class %T { public function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); @@ -38,7 +38,7 @@ public function run() { return $this->length(...); } #[Test] public function class_method() { - $this->verify('class { + $this->verify('class %T { public static function length($arg) { return strlen($arg); } public function run() { return self::length(...); } }'); @@ -46,7 +46,7 @@ public function run() { return self::length(...); } #[Test] public function private_method() { - $this->verify('class { + $this->verify('class %T { private function length($arg) { return strlen($arg); } public function run() { return $this->length(...); } }'); @@ -54,7 +54,7 @@ public function run() { return $this->length(...); } #[Test] public function string_reference() { - $this->verify('class { + $this->verify('class %T { public function run() { $func= "strlen"; return $func(...); @@ -64,7 +64,7 @@ public function run() { #[Test] public function fn_reference() { - $this->verify('class { + $this->verify('class %T { public function run() { $func= fn($arg) => strlen($arg); return $func(...); @@ -74,7 +74,7 @@ public function run() { #[Test] public function instance_property_reference() { - $this->verify('class { + $this->verify('class %T { private $func= "strlen"; public function run() { return ($this->func)(...); @@ -84,7 +84,7 @@ public function run() { #[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])] public function variable_instance_method($expr) { - $this->verify('class { + $this->verify('class %T { private function length($arg) { return strlen($arg); } public function run() { $func= "length"; @@ -95,7 +95,7 @@ public function run() { #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] public function variable_class_method($expr) { - $this->verify('class { + $this->verify('class %T { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; @@ -106,7 +106,7 @@ public function run() { #[Test] public function variable_class_method_with_variable_class() { - $this->verify('class { + $this->verify('class %T { private static function length($arg) { return strlen($arg); } public function run() { $func= "length"; @@ -118,14 +118,14 @@ public function run() { #[Test] public function string_function_reference() { - $this->verify('class { + $this->verify('class %T { public function run() { return "strlen"(...); } }'); } #[Test] public function array_instance_method_reference() { - $this->verify('class { + $this->verify('class %T { public function length($arg) { return strlen($arg); } public function run() { return [$this, "length"](...); } }'); @@ -133,7 +133,7 @@ public function run() { return [$this, "length"](...); } #[Test] public function array_class_method_reference() { - $this->verify('class { + $this->verify('class %T { public static function length($arg) { return strlen($arg); } public function run() { return [self::class, "length"](...); } }'); @@ -141,7 +141,7 @@ public function run() { return [self::class, "length"](...); } #[Test, Expect(Error::class), Values(['nonexistant', '$this->nonexistant', 'self::nonexistant', '$nonexistant', '$null'])] public function non_existant($expr) { - $this->run('class { + $this->run('class %T { public function run() { $null= null; $nonexistant= "nonexistant"; @@ -152,7 +152,7 @@ public function run() { #[Test] public function instantiation() { - $f= $this->run('use lang\ast\unittest\emit\Handle; class { + $f= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { return new Handle(...); } @@ -162,7 +162,7 @@ public function run() { #[Test] public function instantiation_in_map() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { return array_map(new Handle(...), [0, 1, 2]); } @@ -172,7 +172,7 @@ public function run() { #[Test] public function anonymous_instantiation() { - $f= $this->run('class { + $f= $this->run('class %T { public function run() { return new class(...) { public $value; diff --git a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php index 72c81142..45d0b585 100755 --- a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php @@ -11,7 +11,7 @@ public function test() { return 'Test'; } #[Test, Values([0, 1, -1, 0.5, -1.5, '', 'test', true, false])] public function string_cast($value) { Assert::equals((string)$value, $this->run( - 'class { + 'class %T { public function run($value) { return (string)$value; } @@ -23,7 +23,7 @@ public function run($value) { #[Test, Values(['0', '1', '-1', '6100', '', 0.5, -1.5, 0, 1, -1, true, false])] public function int_cast($value) { Assert::equals((int)$value, $this->run( - 'class { + 'class %T { public function run($value) { return (int)$value; } @@ -35,7 +35,7 @@ public function run($value) { #[Test, Values([[[]], [[0, 1, 2]], [['key' => 'value']], null, false, true, 1, 1.5, '', 'test'])] public function array_cast($value) { Assert::equals((array)$value, $this->run( - 'class { + 'class %T { public function run($value) { return (array)$value; } @@ -47,7 +47,7 @@ public function run($value) { #[Test] public function value_cast() { Assert::equals($this, $this->run( - 'class { + 'class %T { public function run($value) { return (\lang\ast\unittest\emit\CastingTest)$value; } @@ -59,7 +59,7 @@ public function run($value) { #[Test] public function int_array_cast() { Assert::equals([1, 2, 3], $this->run( - 'class { + 'class %T { public function run($value) { return (array)$value; } @@ -70,7 +70,7 @@ public function run($value) { #[Test, Expect(ClassCastException::class)] public function cannot_cast_object_to_int_array() { - $this->run('class { + $this->run('class %T { public function run() { return (array)$this; } @@ -80,7 +80,7 @@ public function run() { #[Test, Values([null, 'test'])] public function nullable_string_cast_of($value) { Assert::equals($value, $this->run( - 'class { + 'class %T { public function run($value) { return (?string)$value; } @@ -92,7 +92,7 @@ public function run($value) { #[Test, Values([null, 'test'])] public function nullable_string_cast_of_expression_returning($value) { Assert::equals($value, $this->run( - 'class { + 'class %T { public function run($value) { $values= [$value]; return (?string)array_pop($values); @@ -105,7 +105,7 @@ public function run($value) { #[Test] public function cast_braced() { Assert::equals(['test'], $this->run( - 'class { + 'class %T { public function run($value) { return (array)($value); } @@ -117,7 +117,7 @@ public function run($value) { #[Test] public function cast_to_function_type_and_invoke() { Assert::equals($this->test(), $this->run( - 'class { + 'class %T { public function run($value) { return ((function(): string)($value))(); } @@ -129,7 +129,7 @@ public function run($value) { #[Test] public function object_cast_on_literal() { Assert::equals((object)['key' => 'value'], $this->run( - 'class { + 'class %T { public function run($value) { return (object)["key" => "value"]; } @@ -141,7 +141,7 @@ public function run($value) { #[Test, Values([[1, 1], ['123', 123], [null, null]])] public function nullable_int($value, $expected) { Assert::equals($expected, $this->run( - 'class { + 'class %T { public function run($value) { return (?int)$value; } @@ -153,7 +153,7 @@ public function run($value) { #[Test, Values(eval: '[new Handle(10), null]')] public function nullable_value($value) { Assert::equals($value, $this->run( - 'class { + 'class %T { public function run($value) { return (?\lang\ast\unittest\emit\Handle)$value; } diff --git a/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php index ec830eba..132bc9fa 100755 --- a/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php @@ -12,7 +12,7 @@ class ClassLiteralTest extends EmittingTest { #[Test] public function on_name() { - $r= $this->run('use lang\Primitive; class { + $r= $this->run('use lang\Primitive; class %T { public function run() { return Primitive::class; } }'); Assert::equals(Primitive::class, $r); @@ -20,33 +20,33 @@ public function run() { return Primitive::class; } #[Test] public function on_self() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { return self::class; } }'); - Assert::equals($t->getName(), $t->newInstance()->run()); + Assert::equals($t->literal(), $t->newInstance()->run()); } #[Test] public function on_object() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { return $this::class; } }'); - Assert::equals($t->getName(), $t->newInstance()->run()); + Assert::equals($t->literal(), $t->newInstance()->run()); } #[Test] public function on_instantiation() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { return new self()::class; } }'); - Assert::equals($t->getName(), $t->newInstance()->run()); + Assert::equals($t->literal(), $t->newInstance()->run()); } #[Test] public function on_braced_expression() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { return (new self())::class; } }'); - Assert::equals($t->getName(), $t->newInstance()->run()); + Assert::equals($t->literal(), $t->newInstance()->run()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php index 121f3104..c341f60a 100755 --- a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php @@ -6,31 +6,31 @@ class CommentsTest extends EmittingTest { #[Test] public function on_class() { - $t= $this->type('/** Test */ class { }'); - Assert::equals('Test', $t->getComment()); + $t= $this->declare('/** Test */ class %T { }'); + Assert::equals('Test', $t->comment()); } #[Test] public function on_interface() { - $t= $this->type('/** Test */ interface { }'); - Assert::equals('Test', $t->getComment()); + $t= $this->declare('/** Test */ interface %T { }'); + Assert::equals('Test', $t->comment()); } #[Test] public function on_trait() { - $t= $this->type('/** Test */ trait { }'); - Assert::equals('Test', $t->getComment()); + $t= $this->declare('/** Test */ trait %T { }'); + Assert::equals('Test', $t->comment()); } #[Test] public function on_enum() { - $t= $this->type('/** Test */ enum { }'); - Assert::equals('Test', $t->getComment()); + $t= $this->declare('/** Test */ enum %T { }'); + Assert::equals('Test', $t->comment()); } #[Test] public function on_method() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -38,18 +38,18 @@ public function fixture() { } }'); - Assert::equals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } #[Test] public function comments_are_escaped() { - $t= $this->type("/** Timm's test */ class { }"); - Assert::equals("Timm's test", $t->getComment()); + $t= $this->declare("/** Timm's test */ class %T { }"); + Assert::equals("Timm's test", $t->comment()); } #[Test] public function only_last_comment_is_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Not the right comment */ @@ -59,12 +59,12 @@ public function fixture() { } }'); - Assert::equals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } #[Test] public function next_comment_is_not_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -74,12 +74,12 @@ public function fixture() { /** Not the right comment */ }'); - Assert::equals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } #[Test] public function inline_apidoc_comment_is_not_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -87,6 +87,6 @@ public function fixture() { } }'); - Assert::equals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 917be21d..c42344c4 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -7,7 +7,7 @@ class ControlStructuresTest extends EmittingTest { #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] public function if_else_cascade($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { if (0 === $arg) { return "no items"; @@ -24,7 +24,7 @@ public function run($arg) { #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] public function switch_case($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { switch ($arg) { case 0: return "no items"; @@ -39,7 +39,7 @@ public function run($arg) { #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] public function switch_case_goto_label_ambiguity($whence, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { $position= 1; switch ($arg) { @@ -55,7 +55,7 @@ public function run($arg) { #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] public function switch_case_constant_ambiguity($whence, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { const SET = SEEK_SET; const CURRENT = SEEK_CUR; public function run($arg) { @@ -73,7 +73,7 @@ public function run($arg) { #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items']])] public function match($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { return match ($arg) { 0 => "no items", @@ -88,7 +88,7 @@ public function run($arg) { #[Test, Values([[200, 'OK'], [302, 'Redirect'], [404, 'Error #404']])] public function match_with_multiple_cases($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { return match ($arg) { 200, 201, 202, 203, 204 => "OK", @@ -103,7 +103,7 @@ public function run($arg) { #[Test, Values([['PING', '+PONG'], ['MSG', '+OK Re: Test'], ['XFER', '-ERR Unknown XFER']])] public function match_with_multiple_statements($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($type) { $value= "Test"; return match ($type) { @@ -124,7 +124,7 @@ public function run($type) { #[Test, Values([[0, 'no items'], [1, 'one item'], [5, '5 items'], [10, '10+ items'],])] public function match_with_binary($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { return match (true) { $arg >= 10 => "10+ items", @@ -140,7 +140,7 @@ public function run($arg) { #[Test] public function match_allows_dropping_true() { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { return match { $arg >= 10 => "10+ items", @@ -156,7 +156,7 @@ public function run($arg) { #[Test, Expect(class: Throwable::class, message: '/Unhandled match (value of type .+|case .+)/')] public function unhandled_match() { - $this->run('class { + $this->run('class %T { public function run($arg) { $position= 1; return match ($arg) { @@ -169,7 +169,7 @@ public function run($arg) { #[Test, Expect(class: Throwable::class, message: '/Unknown seek mode .+/')] public function match_with_throw_expression() { - $this->run('class { + $this->run('class %T { public function run($arg) { $position= 1; return match ($arg) { @@ -183,7 +183,7 @@ public function run($arg) { #[Test] public function match_without_arg_inside_fn() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($arg) => match { $arg >= 10 => "10+ items", @@ -199,7 +199,7 @@ public function run() { #[Test] public function match_with_arg_inside_fn() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($arg) => match ($arg) { 0 => "no items", diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php index fd5623ed..0956fdcf 100755 --- a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -7,7 +7,7 @@ class DeclareTest extends EmittingTest { #[Test] public function no_strict_types() { - Assert::equals(1, $this->run('class { + Assert::equals(1, $this->run('class %T { public static function number(int $n) { return $n; } public function run() { return self::number("1"); } }')); @@ -15,7 +15,7 @@ public function run() { return self::number("1"); } #[Test] public function strict_types_off() { - Assert::equals(1, $this->run('declare(strict_types = 0); class { + Assert::equals(1, $this->run('declare(strict_types = 0); class %T { public static function number(int $n) { return $n; } public function run() { return self::number("1"); } }')); @@ -23,7 +23,7 @@ public function run() { return self::number("1"); } #[Test, Expect(class: Error::class, message: '/must be of (the )?type int(eger)?, string given/')] public function strict_types_on() { - $this->run('declare(strict_types = 1); class { + $this->run('declare(strict_types = 1); class %T { public static function number(int $n) { return $n; } public function run() { return self::number("1"); } }'); diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index 97b3215f..0febecf9 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -15,7 +15,7 @@ class EchoTest extends EmittingTest { private function assertEchoes($expected, $statement) { ob_start(); try { - $this->run('class { + $this->run('class %T { private function hello() { return "Hello"; } public function run() { '.$statement.' } }'); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 0b06ae8d..42ca24cb 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -69,35 +69,6 @@ protected function emit($code) { return $out->bytes(); } - /** - * Declare a type - * - * @deprecated Use `declare()` instead - * @param string $code - * @return lang.XPClass - */ - protected function type($code) { - $name= 'T'.(self::$id++); - $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); - if (isset($this->output['ast'])) { - Console::writeLine(); - Console::writeLine('=== ', static::class, ' ==='); - Console::writeLine($tree); - } - - $out= new MemoryOutputStream(); - $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); - if (isset($this->output['code'])) { - Console::writeLine(); - Console::writeLine('=== ', static::class, ' ==='); - Console::writeLine($out->bytes()); - } - - $class= ($package= $tree->scope()->package) ? strtr(substr($package, 1), '\\', '.').'.'.$name : $name; - $this->cl->setClassBytes($class, $out->bytes()); - return $this->cl->loadClass($class); - } - /** * Declare a type with a unique type name (which may be referenced by `%T`) * and return a reflection instance referencing it. @@ -140,7 +111,7 @@ protected function declare($code) { * @return var */ protected function run($code, ... $args) { - return $this->type($code)->newInstance()->run(...$args); + return $this->declare($code)->newInstance()->run(...$args); } #[After] diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 9c5019e5..7fa73299 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -1,6 +1,6 @@ annotations() as $name => $annotation) { + $r[$name]= $annotation->arguments(); + } + return $r; + } + #[Test] public function enum_type() { - Assert::true($this->type('enum { }')->isEnum()); + Assert::equals(Kind::$ENUM, $this->declare('enum %T { }')->kind()); } #[Test] public function name_property() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; @@ -26,12 +40,12 @@ public static function run() { } }'); - Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + Assert::equals('Hearts', $t->method('run')->invoke(null)); } #[Test] public function cases_method_for_unit_enums() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; @@ -40,13 +54,13 @@ public function cases_method_for_unit_enums() { Assert::equals( ['Hearts', 'Diamonds', 'Clubs', 'Spades'], - array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) ); } #[Test] public function cases_method_for_backed_enums() { - $t= $this->type('enum : string { + $t= $this->declare('enum %T: string { case Hearts = "♥"; case Diamonds = "♦"; case Clubs = "♣"; @@ -55,13 +69,13 @@ public function cases_method_for_backed_enums() { Assert::equals( ['Hearts', 'Diamonds', 'Clubs', 'Spades'], - array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) ); } #[Test] public function cases_method_does_not_yield_constants() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; @@ -72,13 +86,13 @@ public function cases_method_does_not_yield_constants() { Assert::equals( ['Hearts', 'Diamonds', 'Clubs', 'Spades'], - array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) ); } #[Test] public function used_as_parameter_default() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case ASC; case DESC; @@ -87,12 +101,12 @@ public static function run($order= self::ASC) { } }'); - Assert::equals('ASC', $t->getMethod('run')->invoke(null)); + Assert::equals('ASC', $t->method('run')->invoke(null)); } #[Test] public function overwritten_parameter_default_value() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case ASC; case DESC; @@ -101,12 +115,12 @@ public static function run($order= self::ASC) { } }'); - Assert::equals('DESC', $t->getMethod('run')->invoke(null, [Enum::valueOf($t, 'DESC')])); + Assert::equals('DESC', $t->method('run')->invoke(null, [Enum::valueOf($t, 'DESC')])); } #[Test] public function value_property_of_backed_enum() { - $t= $this->type('enum : string { + $t= $this->declare('enum %T: string { case ASC = "asc"; case DESC = "desc"; @@ -115,45 +129,45 @@ public static function run() { } }'); - Assert::equals('desc', $t->getMethod('run')->invoke(null)); + Assert::equals('desc', $t->method('run')->invoke(null)); } #[Test, Values([[0, 'NO'], [1, 'YES']])] public function backed_enum_from_int($arg, $expected) { - $t= $this->type('enum : int { + $t= $this->declare('enum %T: int { case NO = 0; case YES = 1; }'); - Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); + Assert::equals($expected, $t->method('from')->invoke(null, [$arg])->name); } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] public function backed_enum_from_string($arg, $expected) { - $t= $this->type('enum : string { + $t= $this->declare('enum %T: string { case ASC = "asc"; case DESC = "desc"; }'); - Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); + Assert::equals($expected, $t->method('from')->invoke(null, [$arg])->name); } #[Test, Expect(class: Error::class, message: '/"illegal" is not a valid backing value for enum .+/')] public function backed_enum_from_nonexistant() { - $t= $this->type('enum : string { + $t= $this->declare('enum %T: string { case ASC = "asc"; case DESC = "desc"; }'); try { - $t->getMethod('from')->invoke(null, ['illegal']); - } catch (TargetInvocationException $e) { + $t->method('from')->invoke(null, ['illegal']); + } catch (InvocationFailed $e) { throw $e->getCause(); } } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] public function backed_enum_tryFrom($arg, $expected) { - $t= $this->type('enum : string { + $t= $this->declare('enum %T: string { case ASC = "asc"; case DESC = "desc"; @@ -162,12 +176,12 @@ public static function run($arg) { } }'); - Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + Assert::equals($expected, $t->method('run')->invoke(null, [$arg])); } #[Test] public function declare_method_on_enum() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; @@ -185,36 +199,36 @@ public static function run() { } }'); - Assert::equals('red', $t->getMethod('run')->invoke(null)); + Assert::equals('red', $t->method('run')->invoke(null)); } #[Test] public function enum_implementing_interface() { - $t= $this->type('use lang\Closeable; enum implements Closeable { + $t= $this->declare('use lang\Closeable; enum %T implements Closeable { case File; case Stream; public function close() { } }'); - Assert::true($t->isSubclassOf('lang.Closeable')); + Assert::true($t->is('lang.Closeable')); } #[Test] public function enum_annotations() { - $t= $this->type('#[Test] enum { }'); - Assert::equals(['test' => null], $t->getAnnotations()); + $t= $this->declare('#[Test] enum %T { }'); + Assert::equals(['Test' => []], $this->annotations($t)); } - #[Test, Ignore('XP core reflection does not support constant annotations')] + #[Test, Ignore('XP reflection does not support constant annotations')] public function enum_member_annotations() { - $t= $this->type('enum { #[Test] case ONE; }'); - Assert::equals(['test' => null], $t->getConstant('ONE')->getAnnotations()); + $t= $this->declare('enum %T { #[Test] case ONE; }'); + Assert::equals(['Test' => []], $this->annotations($t->constant('ONE'))); } #[Test] public function cannot_be_cloned() { - $t= $this->type('use lang\IllegalStateException; enum { + $t= $this->declare('use lang\IllegalStateException; enum %T { case ONE; public static function run() { @@ -229,13 +243,13 @@ public static function run() { Assert::equals( 'Trying to clone an uncloneable object of class '.$t->literal(), - $t->getMethod('run')->invoke(null) + $t->method('run')->invoke(null) ); } #[Test] public function enum_values() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; @@ -250,7 +264,7 @@ public function enum_values() { #[Test] public function enum_value() { - $t= $this->type('enum { + $t= $this->declare('enum %T { case Hearts; case Diamonds; case Clubs; diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 0afb163c..e051c16f 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -7,7 +7,7 @@ class ExceptionsTest extends EmittingTest { #[Test] public function catch_exception() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -22,7 +22,7 @@ public function run() { #[Test] public function line_number_matches() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -37,7 +37,7 @@ public function run() { #[Test] public function catch_without_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -52,7 +52,7 @@ public function run() { #[Test] public function non_capturing_catch() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -67,7 +67,7 @@ public function run() { #[Test] public function finally_without_exception() { - $t= $this->type('class { + $t= $this->declare('class %T { public $closed= false; public function run() { try { @@ -85,7 +85,7 @@ public function run() { #[Test] public function finally_with_exception() { - $t= $this->type('class { + $t= $this->declare('class %T { public $closed= false; public function run() { try { @@ -107,7 +107,7 @@ public function run() { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_this() { - $this->run('class { + $this->run('class %T { private $message= "test"; public function run($user= null) { return $user ?? throw new \\lang\\IllegalArgumentException($this->message); @@ -117,7 +117,7 @@ public function run($user= null) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_null_coalesce() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($user) { return $user ?? throw new \\lang\\IllegalArgumentException("test"); } @@ -127,7 +127,7 @@ public function run($user) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_short_ternary() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($user) { return $user ?: throw new \\lang\\IllegalArgumentException("test"); } @@ -137,7 +137,7 @@ public function run($user) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_normal_ternary() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($user) { return $user ? new User($user) : throw new \\lang\\IllegalArgumentException("test"); } @@ -147,7 +147,7 @@ public function run($user) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_binary_or() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($user) { return $user || throw new \\lang\\IllegalArgumentException("test"); } @@ -157,7 +157,7 @@ public function run($user) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_binary_and() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($error) { return $error && throw new \\lang\\IllegalArgumentException("test"); } @@ -167,7 +167,7 @@ public function run($error) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda() { - $this->run('use lang\IllegalArgumentException; class { + $this->run('use lang\IllegalArgumentException; class %T { public function run() { $f= fn() => throw new IllegalArgumentException("test"); $f(); @@ -177,7 +177,7 @@ public function run() { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_throwing_variable() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($e) { $f= fn() => throw $e; $f(); @@ -188,7 +188,7 @@ public function run($e) { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_variable() { - $this->run('use lang\IllegalArgumentException; class { + $this->run('use lang\IllegalArgumentException; class %T { public function run() { $f= fn($message) => throw new IllegalArgumentException($message); $f("test"); @@ -198,7 +198,7 @@ public function run() { #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_parameter() { - $t= $this->type('use lang\IllegalArgumentException; class { + $t= $this->declare('use lang\IllegalArgumentException; class %T { public function run($message) { $f= fn() => throw new IllegalArgumentException($message); $f(); @@ -209,7 +209,7 @@ public function run($message) { #[Test] public function try_catch_nested_inside_catch() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); diff --git a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php index 63f42a19..346b8478 100755 --- a/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/FunctionTypesTest.class.php @@ -13,25 +13,25 @@ class FunctionTypesTest extends EmittingTest { #[Test] public function function_without_parameters() { - $t= $this->type('class { + $t= $this->declare('class %T { private (function(): string) $test; }'); Assert::equals( new FunctionType([], Primitive::$STRING), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } #[Test] public function function_with_parameters() { - $t= $this->type('class { + $t= $this->declare('class %T { private (function(int, string): string) $test; }'); Assert::equals( new FunctionType([Primitive::$INT, Primitive::$STRING], Primitive::$STRING), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php index d660d108..f300323b 100755 --- a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php @@ -6,7 +6,7 @@ class GotoTest extends EmittingTest { #[Test] public function skip_forward() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { goto skip; return false; @@ -19,7 +19,7 @@ public function run() { #[Test] public function skip_backward() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $return= false; retry: if ($return) return true; diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index f4084a65..e44269c2 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -16,7 +16,7 @@ public function import_type() { Assert::equals(Date::class, $this->run(' use util\Date; - class { + class %T { public function run() { return Date::class; } }' )); @@ -27,7 +27,7 @@ public function import_type_as_alias() { Assert::equals(Date::class, $this->run(' use util\Date as D; - class { + class %T { public function run() { return D::class; } }' )); @@ -38,7 +38,7 @@ public function import_const() { Assert::equals('imported', $this->run(' use const lang\ast\unittest\emit\FIXTURE; - class { + class %T { public function run() { return FIXTURE; } }' )); @@ -49,7 +49,7 @@ public function import_function() { Assert::equals('imported', $this->run(' use function lang\ast\unittest\emit\fixture; - class { + class %T { public function run() { return fixture(); } }' )); @@ -60,7 +60,7 @@ public function import_global_into_namespace() { Assert::equals(Traversable::class, $this->run('namespace test; use Traversable; - class { + class %T { public function run() { return Traversable::class; } }' )); @@ -71,7 +71,7 @@ public function import_globals_into_namespace() { Assert::equals([Traversable::class, Iterator::class], $this->run('namespace test; use Traversable, Iterator; - class { + class %T { public function run() { return [Traversable::class, Iterator::class]; } }' )); diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 4a53f9ad..f7eca64d 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -29,43 +29,43 @@ private function expressions() { #[Test, Values(from: 'expressions')] public function property($declaration, $expected) { - Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { + Assert::equals($expected, $this->run(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { const INITIAL= "initial"; - private $h= %s; + private $h= %D; public function run() { return $this->h; } - }', $declaration))); + }', ['%D' => $declaration]))); } #[Test, Values(from: 'expressions')] public function reflective_access_to_property($declaration, $expected) { - Assert::equals($expected, $this->run(sprintf('use lang\ast\unittest\emit\{FileInput, Handle}; class { + Assert::equals($expected, $this->run(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { const INITIAL= "initial"; - private $h= %s; + private $h= %D; public function run() { return typeof($this)->getField("h")->get($this); } - }', $declaration))); + }', ['%D' => $declaration]))); } #[Test, Values(['fn($arg) => $arg->redirect(1)', 'function($arg) { return $arg->redirect(1); }'])] public function using_closures($declaration) { - $r= $this->run(sprintf('class { - private $h= %s; + $r= $this->run(strtr('class %T { + private $h= %D; public function run() { return $this->h; } - }', $declaration)); + }', ['%D' => $declaration])); Assert::equals(new Handle(1), $r(new Handle(0))); } #[Test] public function using_closures_referencing_this() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private $id= 1; private $h= fn() => new Handle($this->id); @@ -78,7 +78,7 @@ public function run() { #[Test] public function using_new_referencing_this() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private $id= 1; private $h= new Handle($this->id); @@ -91,7 +91,7 @@ public function run() { #[Test] public function using_anonymous_classes() { - $r= $this->run('class { + $r= $this->run('class %T { private $h= new class() { public function pipe($h) { return $h->redirect(1); } }; public function run() { @@ -103,7 +103,7 @@ public function run() { #[Test] public function property_initialization_accessible_inside_constructor() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private $h= new Handle(0); public function __construct() { @@ -119,7 +119,7 @@ public function run() { #[Test] public function promoted_property() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function __construct(private $h= new Handle(0)) { } public function run() { @@ -131,7 +131,7 @@ public function run() { #[Test] public function parameter_default_when_omitted() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run($h= new Handle(0)) { return $h; } @@ -141,7 +141,7 @@ public function run($h= new Handle(0)) { #[Test] public function parameter_default_when_passed() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run($h= new Handle(0)) { return $h; } @@ -151,7 +151,7 @@ public function run($h= new Handle(0)) { #[Test] public function parameter_default_reflective_access() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run($h= new Handle(0)) { return typeof($this)->getMethod("run")->getParameter(0)->getDefaultValue(); } @@ -161,7 +161,7 @@ public function run($h= new Handle(0)) { #[Test] public function property_reference_as_parameter_default() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private static $h= new Handle(0); public function run($h= self::$h) { @@ -173,7 +173,7 @@ public function run($h= self::$h) { #[Test] public function typed_proprety() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private Handle $h= new Handle(0); public function run() { @@ -185,7 +185,7 @@ public function run() { #[Test] public function typed_parameter() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run(Handle $h= new Handle(0)) { return $h; } @@ -195,7 +195,7 @@ public function run(Handle $h= new Handle(0)) { #[Test] public function static_variable() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { static $h= new Handle(0); @@ -207,7 +207,7 @@ public function run() { #[Test] public function with_argument_promotion() { - $t= $this->type('use lang\ast\unittest\emit\Handle; class { + $t= $this->declare('use lang\ast\unittest\emit\Handle; class %T { private $h= new Handle(0); public function __construct(private Handle $p) { } diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index 1659ceb3..d591cbaa 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -6,7 +6,7 @@ class InstanceOfTest extends EmittingTest { #[Test] public function this_is_instanceof_self() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return $this instanceof self; } @@ -17,7 +17,7 @@ public function run() { #[Test] public function new_self_is_instanceof_this() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new self() instanceof $this; } @@ -28,7 +28,7 @@ public function run() { #[Test] public function instanceof_qualified_type() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new \util\Date() instanceof \util\Date; } @@ -39,7 +39,7 @@ public function run() { #[Test] public function instanceof_imported_type() { - $r= $this->run('use util\Date; class { + $r= $this->run('use util\Date; class %T { public function run() { return new Date() instanceof Date; } @@ -50,7 +50,7 @@ public function run() { #[Test] public function instanceof_aliased_type() { - $r= $this->run('use util\Date as D; class { + $r= $this->run('use util\Date as D; class %T { public function run() { return new D() instanceof D; } @@ -61,7 +61,7 @@ public function run() { #[Test] public function instanceof_instance_expr() { - $r= $this->run('class { + $r= $this->run('class %T { private $type= self::class; public function run() { @@ -74,7 +74,7 @@ public function run() { #[Test] public function instanceof_scope_expr() { - $r= $this->run('class { + $r= $this->run('class %T { private static $type= self::class; public function run() { @@ -87,7 +87,7 @@ public function run() { #[Test] public function instanceof_expr() { - $r= $this->run('class { + $r= $this->run('class %T { private function type() { return self::class; } public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php index f035a1a9..6c501047 100755 --- a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php @@ -7,7 +7,7 @@ class InstantiationTest extends EmittingTest { #[Test] public function new_type() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new \\util\\Date(); } @@ -17,7 +17,7 @@ public function run() { #[Test] public function new_var() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $class= \\util\\Date::class; return new $class(); @@ -28,7 +28,7 @@ public function run() { #[Test] public function new_dynamic_var() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $class= \\util\\Date::class; $var= "class"; @@ -40,7 +40,7 @@ public function run() { #[Test] public function new_var_expr() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $class= \\util\\Date::class; $var= "class"; @@ -52,7 +52,7 @@ public function run() { #[Test] public function new_expr() { - $r= $this->run('class { + $r= $this->run('class %T { private function factory() { return \\util\\Date::class; } public function run() { @@ -64,7 +64,7 @@ public function run() { #[Test] public function passing_argument() { - $r= $this->run('class { + $r= $this->run('class %T { public $value; public function __construct($value= null) { $this->value= $value; } diff --git a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php index 83f20c26..82969599 100755 --- a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php @@ -13,73 +13,73 @@ class IntersectionTypesTest extends EmittingTest { #[Test] public function field_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private Traversable&Countable $test; }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } #[Test] public function parameter_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(Traversable&Countable $arg) { } }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getParameter(0)->getType() + $t->method('test')->parameter(0)->constraint()->type() ); } #[Test] public function return_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): Traversable&Countable { } }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getReturnType() + $t->method('test')->returns()->type() ); } #[Test, Runtime(php: '>=8.1.0-dev')] public function field_type_restriction_with_php81() { - $t= $this->type('class { + $t= $this->declare('class %T { private Traversable&Countable $test; }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getField('test')->getTypeRestriction() + $t->property('test')->constraint()->type() ); } - #[Test, Runtime(php: '>=8.1.0-dev')] + #[Test, Runtime(php: '>=8.1.0')] public function parameter_type_restriction_with_php81() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(Traversable&Countable $arg) { } }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getParameter(0)->getTypeRestriction() + $t->method('test')->parameter(0)->constraint()->type() ); } - #[Test, Runtime(php: '>=8.1.0-dev')] + #[Test, Runtime(php: '>=8.1.0')] public function return_type_restriction_with_php81() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): Traversable&Countable { } }'); Assert::equals( new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), - $t->getMethod('test')->getReturnTypeRestriction() + $t->method('test')->returns()->type() ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index b5ad3360..15ae41bf 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -8,7 +8,7 @@ class InvocationTest extends EmittingTest { #[Test] public function instance_method() { Assert::equals('instance', $this->run( - 'class { + 'class %T { public function instanceMethod() { return "instance"; } @@ -22,7 +22,7 @@ public function run() { #[Test] public function instance_method_dynamic_variable() { Assert::equals('instance', $this->run( - 'class { + 'class %T { public function instanceMethod() { return "instance"; } @@ -37,7 +37,7 @@ public function run() { #[Test] public function instance_method_dynamic_expression() { Assert::equals('instance', $this->run( - 'class { + 'class %T { public function instanceMethod() { return "instance"; } @@ -52,7 +52,7 @@ public function run() { #[Test] public function static_method() { Assert::equals('static', $this->run( - 'class { + 'class %T { public function staticMethod() { return "static"; } @@ -66,7 +66,7 @@ public function run() { #[Test] public function static_method_dynamic() { Assert::equals('static', $this->run( - 'class { + 'class %T { public static function staticMethod() { return "static"; } @@ -81,7 +81,7 @@ public function run() { #[Test] public function closure() { Assert::equals('closure', $this->run( - 'class { + 'class %T { public function run() { $f= function() { return "closure"; }; @@ -95,7 +95,7 @@ public function run() { public function global_function() { Assert::equals('function', $this->run( 'function fixture() { return "function"; } - class { + class %T { public function run() { return fixture(); @@ -107,7 +107,7 @@ public function run() { #[Test] public function function_self_reference() { Assert::equals(13, $this->run( - 'class { + 'class %T { public function run() { $fib= function($i) use(&$fib) { @@ -126,7 +126,7 @@ public function run() { #[Test, Values(['"html(<) = <", flags: ENT_HTML5', '"html(<) = <", ENT_HTML5, double: true', 'string: "html(<) = <", flags: ENT_HTML5', 'string: "html(<) = <", flags: ENT_HTML5, double: true',])] public function named_arguments_in_exact_order($arguments) { Assert::equals('html(<) = &lt;', $this->run( - 'class { + 'class %T { public function escape($string, $flags= ENT_HTML5, $double= true) { return htmlspecialchars($string, $flags, null, $double); @@ -142,7 +142,7 @@ public function run() { #[Test, Runtime(php: '>=8.0')] public function named_arguments_in_reverse_order() { Assert::equals('html(<) = &lt;', $this->run( - 'class { + 'class %T { public function escape($string, $flags= ENT_HTML5, $double= true) { return htmlspecialchars($string, $flags, null, $double); @@ -158,7 +158,7 @@ public function run() { #[Test, Runtime(php: '>=8.0')] public function named_arguments_omitting_one() { Assert::equals('html(<) = <', $this->run( - 'class { + 'class %T { public function escape($string, $flags= ENT_HTML5, $double= true) { return htmlspecialchars($string, $flags, null, $double); diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 8a0a1db0..93964962 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -15,7 +15,7 @@ class LambdasTest extends EmittingTest { #[Test] public function inc() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($a) => $a + 1; } @@ -26,7 +26,7 @@ public function run() { #[Test] public function add() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($a, $b) => $a + $b; } @@ -37,7 +37,7 @@ public function run() { #[Test] public function captures_this() { - $r= $this->run('class { + $r= $this->run('class %T { private $addend= 2; public function run() { @@ -50,7 +50,7 @@ public function run() { #[Test, Condition(assert: 'property_exists(LambdaExpression::class, "static")')] public function static_fn_does_not_capture_this() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return static fn() => isset($this); } @@ -61,7 +61,7 @@ public function run() { #[Test, Condition(assert: 'property_exists(ClosureExpression::class, "static")')] public function static_function_does_not_capture_this() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return static function() { return isset($this); }; } @@ -72,7 +72,7 @@ public function run() { #[Test] public function captures_local() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; return fn($a) => $a + $addend; @@ -84,7 +84,7 @@ public function run() { #[Test] public function captures_local_from_use_list() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; $f= function() use($addend) { @@ -99,7 +99,7 @@ public function run() { #[Test] public function captures_local_from_lambda() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; $f= fn() => fn($a) => $a + $addend; @@ -112,7 +112,7 @@ public function run() { #[Test] public function captures_local_assigned_via_list() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [$addend]= [2]; return fn($a) => $a + $addend; @@ -124,7 +124,7 @@ public function run() { #[Test] public function captures_param() { - $r= $this->run('class { + $r= $this->run('class %T { public function run($addend) { return fn($a) => $a + $addend; } @@ -135,7 +135,7 @@ public function run($addend) { #[Test] public function captures_braced_local() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; return fn($a) => $a + ($addend); @@ -147,7 +147,7 @@ public function run() { #[Test] public function typed_parameters() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn(\\lang\\Value $in) => $in; } @@ -158,7 +158,7 @@ public function run() { #[Test] public function typed_return() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($in): \\lang\\Value => $in; } @@ -169,7 +169,7 @@ public function run() { #[Test] public function without_params() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn() => 1; } @@ -180,7 +180,7 @@ public function run() { #[Test] public function immediately_invoked_function_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return (fn() => "IIFE")(); } @@ -191,7 +191,7 @@ public function run() { #[Test] public function with_block() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn() => { $a= 1; @@ -205,7 +205,7 @@ public function run() { #[Test] public function capturing_with_block() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $a= 1; return fn() => { @@ -219,7 +219,7 @@ public function run() { #[Test, Expect(Errors::class)] public function no_longer_supports_hacklang_variant() { - $this->run('class { + $this->run('class %T { public function run() { $func= ($arg) ==> { return 1; }; } diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index ecba2112..4b0f40ad 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -6,7 +6,7 @@ class LoopsTest extends EmittingTest { #[Test] public function foreach_with_value() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([1, 2, 3] as $number) { @@ -21,7 +21,7 @@ public function run() { #[Test] public function foreach_with_key_and_value() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach (["a" => 1, "b" => 2, "c" => 3] as $key => $number) { @@ -36,7 +36,7 @@ public function run() { #[Test] public function foreach_with_single_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([1, 2, 3] as $number) $result.= ",".$number; @@ -49,7 +49,7 @@ public function run() { #[Test] public function foreach_with_destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([[1, 2], [3, 4]] as [$a, $b]) $result.= ",".$a." & ".$b; @@ -62,7 +62,7 @@ public function run() { #[Test] public function foreach_with_destructuring_and_missing_expressions() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([[1, 2, 3], [4, 5, 6]] as [$a, , $b]) $result.= ",".$a." & ".$b; @@ -75,7 +75,7 @@ public function run() { #[Test] public function foreach_with_destructuring_keys() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([["a" => 1, "b" => 2], ["a" => 3, "b" => 4]] as ["a" => $a, "b" => $b]) $result.= ",".$a." & ".$b; @@ -88,7 +88,7 @@ public function run() { #[Test] public function foreach_with_destructuring_references() { - $r= $this->run('class { + $r= $this->run('class %T { private $list= [[1, 2], [3, 4]]; public function run() { @@ -105,7 +105,7 @@ public function run() { #[Test] public function for_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; for ($i= 1; $i < 4; $i++) { @@ -120,7 +120,7 @@ public function run() { #[Test] public function while_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; $i= 0; @@ -136,7 +136,7 @@ public function run() { #[Test] public function do_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; $i= 1; @@ -152,7 +152,7 @@ public function run() { #[Test] public function break_while_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $i= 0; $r= []; @@ -172,7 +172,7 @@ public function run() { #[Test] public function continue_while_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $i= 0; $r= []; diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index d05066dd..834b1740 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -7,7 +7,7 @@ class MembersTest extends EmittingTest { #[Test] public function class_property() { - $r= $this->run('class { + $r= $this->run('class %T { private static $MEMBER= "Test"; public function run() { @@ -20,7 +20,7 @@ public function run() { #[Test] public function typed_class_property() { - $r= $this->run('class { + $r= $this->run('class %T { private static string $MEMBER= "Test"; public function run() { @@ -33,7 +33,7 @@ public function run() { #[Test] public function class_method() { - $r= $this->run('class { + $r= $this->run('class %T { private static function member() { return "Test"; } public function run() { @@ -46,7 +46,7 @@ public function run() { #[Test] public function class_constant() { - $r= $this->run('class { + $r= $this->run('class %T { private const MEMBER = "Test"; public function run() { @@ -59,9 +59,9 @@ public function run() { #[Test] public function typed_class_constant() { - $t= Reflection::type($this->type('class { + $t= $this->declare('class %T { private const string MEMBER = "Test"; - }')); + }'); $const= $t->constant('MEMBER'); Assert::equals('Test', $const->value()); @@ -70,7 +70,7 @@ public function typed_class_constant() { #[Test, Values(['$this->$member', '$this->{$member}', '$this->{strtoupper($member)}'])] public function dynamic_instance_property($syntax) { - $r= $this->run('class { + $r= $this->run('class %T { private $MEMBER= "Test"; public function run() { @@ -84,7 +84,7 @@ public function run() { #[Test, Values(['self::$$member', 'self::${$member}', 'self::${strtoupper($member)}'])] public function dynamic_class_property($syntax) { - $r= $this->run('class { + $r= $this->run('class %T { private static $MEMBER= "Test"; public function run() { @@ -98,7 +98,7 @@ public function run() { #[Test, Values(['$this->$method()', '$this->{$method}()', '$this->{strtolower($method)}()'])] public function dynamic_instance_method($syntax) { - $r= $this->run('class { + $r= $this->run('class %T { private function test() { return "Test"; } public function run() { @@ -112,7 +112,7 @@ public function run() { #[Test, Values(['self::$method()', 'self::{$method}()', 'self::{strtolower($method)}()'])] public function dynamic_class_method($syntax) { - $r= $this->run('class { + $r= $this->run('class %T { private static function test() { return "Test"; } public function run() { @@ -126,7 +126,7 @@ public function run() { #[Test, Values(['self::{$member}', 'self::{strtoupper($member)}'])] public function dynamic_class_constant($syntax) { - $r= $this->run('class { + $r= $this->run('class %T { const MEMBER= "Test"; public function run() { @@ -140,7 +140,7 @@ public function run() { #[Test] public function property_of_dynamic_class() { - $r= $this->run('class { + $r= $this->run('class %T { private static $MEMBER= "Test"; public function run() { @@ -154,7 +154,7 @@ public function run() { #[Test] public function method_of_dynamic_class() { - $r= $this->run('class { + $r= $this->run('class %T { private static function member() { return "Test"; } public function run() { @@ -168,7 +168,7 @@ public function run() { #[Test] public function constant_of_dynamic_class() { - $r= $this->run('class { + $r= $this->run('class %T { private const MEMBER = "Test"; public function run() { @@ -182,7 +182,7 @@ public function run() { #[Test] public function object_class_constant() { - $r= $this->run('class { + $r= $this->run('class %T { private const MEMBER = "Test"; public function run() { @@ -195,7 +195,7 @@ public function run() { #[Test] public function list_property() { - $r= $this->run('class { + $r= $this->run('class %T { private $list= [1, 2, 3]; public function run() { @@ -208,7 +208,7 @@ public function run() { #[Test] public function list_method() { - $r= $this->run('class { + $r= $this->run('class %T { private function list() { return [1, 2, 3]; } public function run() { @@ -221,7 +221,7 @@ public function run() { #[Test] public function return_by_reference() { - $r= $this->run('class { + $r= $this->run('class %T { private $list= []; public function &list() { return $this->list; } @@ -238,7 +238,7 @@ public function run() { #[Test, Values(['variable', 'invocation', 'array'])] public function class_on_objects($via) { - $t= $this->type('class { + $t= $this->declare('class %T { private function this() { return $this; } public function variable() { return $this::class; } @@ -249,12 +249,12 @@ public function array() { return [$this][0]::class; } }'); $fixture= $t->newInstance(); - Assert::equals(get_class($fixture), $t->getMethod($via)->invoke($fixture)); + Assert::equals(get_class($fixture), $t->method($via)->invoke($fixture)); } #[Test] public function instance_property() { - $r= $this->run('class { + $r= $this->run('class %T { private $member= "Test"; public function run() { @@ -267,7 +267,7 @@ public function run() { #[Test] public function typed_instance_property() { - $r= $this->run('class { + $r= $this->run('class %T { private string $member= "Test"; public function run() { @@ -280,7 +280,7 @@ public function run() { #[Test] public function instance_method() { - $r= $this->run('class { + $r= $this->run('class %T { private function member() { return "Test"; } public function run() { @@ -293,7 +293,7 @@ public function run() { #[Test] public function static_initializer_run() { - $r= $this->run('class { + $r= $this->run('class %T { private static $MEMBER; static function __static() { @@ -310,7 +310,7 @@ public function run() { #[Test] public function enum_members() { - $r= $this->run('class extends \lang\Enum { + $r= $this->run('class %T extends \lang\Enum { public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; public function run() { @@ -323,11 +323,11 @@ public function run() { #[Test] public function allow_constant_syntax_for_members() { - $r= $this->run('use lang\{Enum, CommandLine}; class extends Enum { + $r= $this->run('use lang\{Enum, CommandLine}; class %T extends Enum { public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; public function run() { - return [self::MON->name(), ::TUE->name(), CommandLine::WINDOWS->name()]; + return [self::MON->name(), %T::TUE->name(), CommandLine::WINDOWS->name()]; } }'); @@ -336,7 +336,7 @@ public function run() { #[Test] public function method_with_static() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { static $var= "Test"; return $var; @@ -348,7 +348,7 @@ public function run() { #[Test] public function method_with_static_without_initializer() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { static $var; return $var; @@ -360,7 +360,7 @@ public function run() { #[Test] public function chaining_sccope_operators() { - $r= $this->run('class { + $r= $this->run('class %T { private const TYPE = self::class; private const NAME = "Test"; @@ -380,26 +380,26 @@ public function run() { #[Test] public function self_return_type() { - $t= $this->type(' - class { public function run(): self { return $this; } } + $t= $this->declare(' + class %T { public function run(): self { return $this; } } '); - Assert::equals($t, $t->getMethod('run')->getReturnType()); + Assert::equals($t->class(), $t->method('run')->returns()->type()); } #[Test] public function static_return_type() { - $t= $this->type(' - class Base { public function run(): static { return $this; } } - class extends Base { } + $t= $this->declare(' + class %TBase { public function run(): static { return $this; } } + class %T extends %TBase { } '); - Assert::equals($t, $t->getMethod('run')->getReturnType()); + Assert::equals($t->parent()->class(), $t->method('run')->returns()->type()); } #[Test] public function array_of_self_return_type() { - $t= $this->type(' - class { public function run(): array { return [$this]; } } + $t= $this->declare(' + class %T { public function run(): array { return [$this]; } } '); - Assert::equals(new ArrayType($t), $t->getMethod('run')->getReturnType()); + Assert::equals(new ArrayType($t->class()), $t->method('run')->returns()->type()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php index debc2236..bcad3634 100755 --- a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php @@ -12,7 +12,7 @@ class MultipleCatchTest extends EmittingTest { #[Test, Values([IllegalArgumentException::class, IllegalStateException::class])] public function catch_both($type) { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($t) { try { throw new $t("test"); diff --git a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php index 72b7da0f..7fe7dcc7 100755 --- a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php @@ -7,17 +7,17 @@ class NamespacesTest extends EmittingTest { #[Test] public function without_namespace() { - Assert::equals('', $this->type('class { }')->getPackage()->getName()); + Assert::null($this->declare('class %T { }')->package()); } #[Test] public function with_namespace() { - Assert::equals('test', $this->type('namespace test; class { }')->getPackage()->getName()); + Assert::equals('test', $this->declare('namespace test; class %T { }')->package()->name()); } #[Test] public function resolves_unqualified() { - $r= $this->run('namespace util; class { + $r= $this->run('namespace util; class %T { public function run() { return new Date("1977-12-14"); } @@ -27,7 +27,7 @@ public function run() { #[Test] public function resolves_relative() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new util\Date("1977-12-14"); } @@ -37,7 +37,7 @@ public function run() { #[Test] public function resolves_absolute() { - $r= $this->run('namespace test; class { + $r= $this->run('namespace test; class %T { public function run() { return new \util\Date("1977-12-14"); } @@ -47,7 +47,7 @@ public function run() { #[Test] public function resolves_import() { - $r= $this->run('namespace test; use util\Date; class { + $r= $this->run('namespace test; use util\Date; class %T { public function run() { return new Date("1977-12-14"); } @@ -57,7 +57,7 @@ public function run() { #[Test] public function resolves_alias() { - $r= $this->run('namespace test; use util\Date as DateTime; class { + $r= $this->run('namespace test; use util\Date as DateTime; class %T { public function run() { return new DateTime("1977-12-14"); } @@ -67,7 +67,7 @@ public function run() { #[Test] public function resolves_namespace_keyword() { - $r= $this->run('namespace util; class { + $r= $this->run('namespace util; class %T { public function run() { return new namespace\Date("1977-12-14"); } @@ -77,7 +77,7 @@ public function run() { #[Test] public function resolves_sub_namespace() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new namespace\util\Date("1977-12-14"); } diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php index 7ab5aa2d..2a9eb74f 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php @@ -6,7 +6,7 @@ class NullCoalesceAssignmentTest extends EmittingTest { #[Test, Values([[null, true], [false, false], ['Test', 'Test']])] public function assigns_true_if_null($value, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { $arg??= true; return $arg; @@ -18,7 +18,7 @@ public function run($arg) { #[Test, Values([[[], true], [[null], true], [[false], false], [['Test'], 'Test']])] public function fills_array_if_non_existant_or_null($value, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run($arg) { $arg[0]??= true; return $arg; diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php index a57dc9ca..0d15125f 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php @@ -6,7 +6,7 @@ class NullCoalesceTest extends EmittingTest { #[Test] public function on_null() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return null ?? true; } @@ -17,7 +17,7 @@ public function run() { #[Test] public function on_unset_array_key() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return $array["key"] ?? true; } @@ -28,7 +28,7 @@ public function run() { #[Test] public function assignment_operator() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $array["key"] ??= true; return $array; diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index 5ada8e9e..f095031b 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -14,7 +14,7 @@ class NullSafeTest extends EmittingTest { #[Test] public function method_call_on_null() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= null; return $object?->method(); @@ -26,7 +26,7 @@ public function run() { #[Test] public function method_call_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public function method() { return true; } @@ -40,7 +40,7 @@ public function method() { return true; } #[Test] public function member_access_on_null() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= null; return $object?->member; @@ -52,7 +52,7 @@ public function run() { #[Test] public function member_access_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public $member= true; @@ -67,17 +67,17 @@ public function run() { #[Test] public function chained_method_call() { $r= $this->run(' - class Invocation { + class %TInvocation { public static $invoked= []; public function __construct(private $name, private $chained) { } public function chained() { self::$invoked[]= $this->name; return $this->chained; } } - class { + class %T { public function run() { - $invokation= new Invocation("outer", new Invocation("inner", null)); + $invokation= new %TInvocation("outer", new %TInvocation("inner", null)); $return= $invokation?->chained()?->chained()?->chained(); - return [$return, Invocation::$invoked]; + return [$return, %TInvocation::$invoked]; } } '); @@ -87,7 +87,7 @@ public function run() { #[Test] public function dynamic_member_access_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public $member= true; @@ -104,7 +104,7 @@ public function name() { return "member"; } #[Test] public function short_circuiting_chain() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $null= null; return $null?->method($undefined->method()); @@ -116,7 +116,7 @@ public function run() { #[Test] public function short_circuiting_parameter() { - $r= $this->run('class { + $r= $this->run('class %T { private function pass($object) { return $object; } diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 46f6d985..aebf93b0 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -7,16 +7,30 @@ class ParameterTest extends EmittingTest { use NullableSupport; + /** + * Returns annotations present in the given type + * + * @param lang.reflection.Annotated $annotated + * @return [:var[]] + */ + private function annotations($annotated) { + $r= []; + foreach ($annotated->annotations() as $name => $annotation) { + $r[$name]= $annotation->arguments(); + } + return $r; + } + /** * Helper to declare a type and return a parameter reflection object * * @param string $declaration - * @return lang.reflect.Parameter + * @return lang.reflection.Parameter */ private function param($declaration) { - return $this->type('use lang\Value; class { public function fixture('.$declaration.') { } }') - ->getMethod('fixture') - ->getParameter(0) + return $this->declare('use lang\Value; class %T { public function fixture('.$declaration.') { } }') + ->method('fixture') + ->parameter(0) ; } @@ -30,113 +44,104 @@ private function special() { #[Test] public function name() { - Assert::equals('param', $this->param('$param')->getName()); + Assert::equals('param', $this->param('$param')->name()); } #[Test] public function without_type() { - Assert::equals(Type::$VAR, $this->param('$param')->getType()); + Assert::equals(Type::$VAR, $this->param('$param')->constraint()->type()); } #[Test, Values(from: 'special')] public function with_special_type($declaration, $type) { - Assert::equals($type, $this->param($declaration)->getType()); + Assert::equals($type, $this->param($declaration)->constraint()->type()); } #[Test] public function value_typed() { - Assert::equals(new XPClass(Value::class), $this->param('Value $param')->getType()); + Assert::equals(new XPClass(Value::class), $this->param('Value $param')->constraint()->type()); } #[Test] public function value_type_with_null() { - Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('Value $param= null')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('Value $param= null')->constraint()->type()); } #[Test] public function nullable_value_type() { - Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param')->constraint()->type()); } #[Test] public function string_typed() { - Assert::equals(Primitive::$STRING, $this->param('string $param')->getType()); + Assert::equals(Primitive::$STRING, $this->param('string $param')->constraint()->type()); } #[Test] public function string_typed_with_null() { - Assert::equals($this->nullable(Primitive::$STRING), $this->param('string $param= null')->getType()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('string $param= null')->constraint()->type()); } #[Test] public function nullable_string_type() { - Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getType()); - } - - #[Test, Runtime(php: '>=7.1')] - public function nullable_string_type_restriction() { - Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->getTypeRestriction()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->constraint()->type()); } #[Test] public function array_typed() { - Assert::equals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); - } - - #[Test] - public function array_typed_restriction() { - Assert::equals(Type::$ARRAY, $this->param('array $param')->getTypeRestriction()); + Assert::equals( + new ArrayType(Primitive::$INT), + $this->param('array $param')->constraint()->type() + ); } #[Test] public function map_typed() { - Assert::equals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); - } - - #[Test] - public function map_typed_restriction() { - Assert::equals(Type::$ARRAY, $this->param('array $param')->getTypeRestriction()); + Assert::equals( + new MapType(Primitive::$INT), + $this->param('array $param')->constraint()->type() + ); } #[Test] public function simple_annotation() { - Assert::equals(['inject' => null], $this->param('#[Inject] $param')->getAnnotations()); + Assert::equals(['Inject' => []], $this->annotations($this->param('#[Inject] $param'))); } #[Test] public function annotation_with_value() { - Assert::equals(['inject' => 'dsn'], $this->param('#[Inject("dsn")] $param')->getAnnotations()); + Assert::equals(['Inject' => ['dsn']], $this->annotations($this->param('#[Inject("dsn")] $param'))); } #[Test] public function multiple_annotations() { Assert::equals( - ['inject' => null, 'name' => 'dsn'], - $this->param('#[Inject, Name("dsn")] $param')->getAnnotations() + ['Inject' => [], 'Name' => ['dsn']], + $this->annotations($this->param('#[Inject, Name("dsn")] $param')) ); } #[Test] public function required_parameter() { - Assert::equals(false, $this->param('$param')->isOptional()); + Assert::equals(false, $this->param('$param')->optional()); } #[Test] public function optional_parameter() { - Assert::equals(true, $this->param('$param= true')->isOptional()); + Assert::equals(true, $this->param('$param= true')->optional()); } #[Test] public function optional_parameters_default_value() { - Assert::equals(true, $this->param('$param= true')->getDefaultValue()); + Assert::equals(true, $this->param('$param= true')->default()); } #[Test] public function trailing_comma_allowed() { - $p= $this->type('class { public function fixture($param, ) { } }') - ->getMethod('fixture') - ->getParameters() + $p= $this->declare('class %T { public function fixture($param, ) { } }') + ->method('fixture') + ->parameters() ; - Assert::equals(1, sizeof($p), 'number of parameters'); + Assert::equals(1, $p->size(), 'number of parameters'); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php index 27bf9b57..91ec35ed 100755 --- a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -7,7 +7,7 @@ class PrecedenceTest extends EmittingTest { #[Test, Values([['2 + 3 * 4', 14], ['2 + 8 / 4', 4], ['2 + 3 ** 2', 11], ['2 + 5 % 2', 3]])] public function mathematical($input, $result) { Assert::equals($result, $this->run( - 'class { + 'class %T { public function run() { return '.$input.'; } @@ -17,8 +17,8 @@ public function run() { #[Test] public function concatenation() { - $t= $this->type( - 'class { + $t= $this->declare( + 'class %T { public function run() { return "(".self::class.")"; } @@ -29,8 +29,8 @@ public function run() { #[Test] public function plusplus() { - $t= $this->type( - 'class { + $t= $this->declare( + 'class %T { private $number= 1; public function run() { diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index 744da360..09d481d7 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -1,5 +1,6 @@ type('class { + $t= $this->declare('class %T { private int $test; }'); - Assert::equals('int', $t->getField('test')->getTypeName()); + Assert::equals(Primitive::$INT, $t->property('test')->constraint()->type()); } #[Test] public function self_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private static self $instance; }'); - Assert::equals('self', $t->getField('instance')->getTypeName()); + Assert::equals($t->class(), $t->property('instance')->constraint()->type()); } #[Test] public function interface_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private \\lang\\Value $value; }'); - Assert::equals('lang.Value', $t->getField('value')->getTypeName()); + Assert::equals(XPClass::forName('lang.Value'), $t->property('value')->constraint()->type()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index c5167931..9c8d1db5 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -22,57 +22,57 @@ private function modifiers() { #[Test] public function class_declaration() { - $t= $this->type('readonly class { + $t= $this->declare('readonly class %T { public int $fixture; }'); Assert::equals( - sprintf('public readonly int %s::$fixture', $t->getName()), - $t->getField('fixture')->toString() + 'public readonly int $fixture', + $t->property('fixture')->toString() ); } #[Test] public function property_declaration() { - $t= $this->type('class { + $t= $this->declare('class %T { public readonly int $fixture; }'); Assert::equals( - sprintf('public readonly int %s::$fixture', $t->getName()), - $t->getField('fixture')->toString() + 'public readonly int $fixture', + $t->property('fixture')->toString() ); } #[Test] public function class_with_constructor_argument_promotion() { - $t= $this->type('readonly class { + $t= $this->declare('readonly class %T { public function __construct(public string $fixture) { } }'); Assert::equals( - sprintf('public readonly string %s::$fixture', $t->getName()), - $t->getField('fixture')->toString() + 'public readonly string $fixture', + $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); } #[Test] public function property_defined_with_constructor_argument_promotion() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(public readonly string $fixture) { } }'); Assert::equals( - sprintf('public readonly string %s::$fixture', $t->getName()), - $t->getField('fixture')->toString() + 'public readonly string $fixture', + $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); } #[Test, Values(from: 'modifiers')] public function reading_from_class($modifiers) { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct('.$modifiers.' readonly string $fixture) { } public function run() { return $this->fixture; } @@ -82,7 +82,7 @@ public function run() { return $this->fixture; } #[Test] public function reading_public_from_outside() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(public readonly string $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); @@ -90,10 +90,10 @@ public function __construct(public readonly string $fixture) { } #[Test] public function reading_protected_from_subclass() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(protected readonly string $fixture) { } }'); - $i= newinstance($t->getName(), ['Test'], [ + $i= newinstance($t->name(), ['Test'], [ 'run' => function() { return $this->fixture; } ]); Assert::equals('Test', $i->run()); @@ -101,7 +101,7 @@ public function __construct(protected readonly string $fixture) { } #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] public function cannot_read_protected() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(protected readonly string $fixture) { } }'); $t->newInstance('Test')->fixture; @@ -109,7 +109,7 @@ public function __construct(protected readonly string $fixture) { } #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] public function cannot_write_protected() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(protected readonly string $fixture) { } }'); $t->newInstance('Test')->fixture= 'Modified'; @@ -117,7 +117,7 @@ public function __construct(protected readonly string $fixture) { } #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] public function cannot_read_private() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(private readonly string $fixture) { } }'); $t->newInstance('Test')->fixture; @@ -125,7 +125,7 @@ public function __construct(private readonly string $fixture) { } #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] public function cannot_write_private() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(private readonly string $fixture) { } }'); $t->newInstance('Test')->fixture= 'Modified'; @@ -133,7 +133,7 @@ public function __construct(private readonly string $fixture) { } #[Test] public function assigning_inside_constructor() { - $t= $this->type('class { + $t= $this->declare('class %T { public readonly string $fixture; public function __construct($fixture) { $this->fixture= $fixture; } }'); @@ -142,18 +142,18 @@ public function __construct($fixture) { $this->fixture= $fixture; } #[Test] public function can_be_assigned_via_reflection() { - $t= $this->type('class { + $t= $this->declare('class %T { public readonly string $fixture; }'); $i= $t->newInstance(); - $t->getField('fixture')->setAccessible(true)->set($i, 'Test'); + $t->property('fixture')->set($i, 'Test'); Assert::equals('Test', $i->fixture); } #[Test, Expect(class: Error::class, message: '/Cannot initialize readonly property .+fixture/')] public function cannot_initialize_from_outside() { - $t= $this->type('class { + $t= $this->declare('class %T { public readonly string $fixture; }'); $t->newInstance()->fixture= 'Test'; @@ -161,7 +161,7 @@ public function cannot_initialize_from_outside() { #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] public function cannot_be_set_after_initialization() { - $t= $this->type('class { + $t= $this->declare('class %T { public function __construct(public readonly string $fixture) { } }'); $t->newInstance('Test')->fixture= 'Modified'; @@ -169,26 +169,26 @@ public function __construct(public readonly string $fixture) { } #[Test, Ignore('Until proper error handling facilities exist')] public function cannot_have_an_initial_value() { - $this->type('class { + $this->declare('class %T { public readonly string $fixture= "Test"; }'); } #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] public function cannot_read_dynamic_members_from_readonly_classes() { - $t= $this->type('readonly class { }'); + $t= $this->declare('readonly class %T { }'); $t->newInstance()->fixture; } #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] public function cannot_write_dynamic_members_from_readonly_classes() { - $t= $this->type('readonly class { }'); + $t= $this->declare('readonly class %T { }'); $t->newInstance()->fixture= true; } #[Test, Ignore('Until proper error handling facilities exist')] public function readonly_classes_cannot_have_static_members() { - $this->type('readonly class { + $this->declare('readonly class %T { public static $test; }'); } diff --git a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php index 6b1ed68b..1b2fb1c5 100755 --- a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php @@ -6,7 +6,7 @@ class ReturnTest extends EmittingTest { #[Test] public function return_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return "Test"; } @@ -16,7 +16,7 @@ public function run() { #[Test] public function return_member() { - $r= $this->run('class { + $r= $this->run('class %T { private $member= "Test"; public function run() { @@ -28,7 +28,7 @@ public function run() { #[Test] public function return_without_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return; } diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index 7cea2acf..8ad1c0fe 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -6,36 +6,36 @@ class ScalarsTest extends EmittingTest { #[Test, Values([['0', 0], ['1', 1], ['-1', -1], ['1.5', 1.5], ['-1.5', -1.5],])] public function numbers($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['0b0', 0], ['0b10', 2], ['0B10', 2]])] public function binary_numbers($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['0x0', 0], ['0xff', 255], ['0xFF', 255], ['0XFF', 255]])] public function hexadecimal_numbers($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['0755', 493], ['0o16', 14], ['0O16', 14]])] public function octal_numbers($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['135_99', 13599], ['107_925_284.88', 107925284.88], ['0xCAFE_F00D', 3405705229], ['0b0101_1111', 95], ['0137_041', 48673],])] public function numeric_literal_separator($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['""', ''], ['"Test"', 'Test'],])] public function strings($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } #[Test, Values([['true', true], ['false', false], ['null', null],])] public function constants($literal, $result) { - Assert::equals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php b/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php index 6346cecf..91a8a8e5 100755 --- a/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/StaticLocalsTest.class.php @@ -1,6 +1,6 @@ getMethod('run')->invoke($t->newInstance(), $args); + return $t->method('run')->invoke($t->newInstance(), $args); } #[Test] public function constant_static() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { static $i= 0; @@ -35,7 +35,7 @@ public function run() { #[Test] public function initialization_to_new() { - $t= $this->type('use util\\{Date, Dates}; class { + $t= $this->declare('use util\\{Date, Dates}; class %T { public function run() { static $t= new Date(0); @@ -49,7 +49,7 @@ public function run() { #[Test] public function initialization_to_parameter() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($initial) { static $t= $initial; @@ -64,7 +64,7 @@ public function run($initial) { #[Test] public function initialization_when_throwing() { - $t= $this->type('use lang\\IllegalArgumentException; class { + $t= $this->declare('use lang\\IllegalArgumentException; class %T { public function run($initial) { static $t= $initial ?? throw new IllegalArgumentException("May not be null"); @@ -73,7 +73,7 @@ public function run($initial) { }'); // This does not initialize the static - Assert::throws(TargetInvocationException::class, function() use($t) { + Assert::throws(InvocationFailed::class, function() use($t) { $this->apply($t, null); }); diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index b397dd06..39e9eb13 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -8,7 +8,7 @@ class TernaryTest extends EmittingTest { #[Test, Values([[true, 'OK'], [false, 'Fail']])] public function ternary($value, $result) { Assert::equals($result, $this->run( - 'class { + 'class %T { public function run($value) { return $value ? "OK" : "Fail"; } @@ -20,7 +20,7 @@ public function run($value) { #[Test, Values([[true, MODIFIER_PUBLIC], [false, MODIFIER_PRIVATE]])] public function ternary_constants_goto_label_ambiguity($value, $result) { Assert::equals($result, $this->run( - 'class { + 'class %T { public function run($value) { return $value ? MODIFIER_PUBLIC : MODIFIER_PRIVATE; } @@ -32,7 +32,7 @@ public function run($value) { #[Test, Values([['OK', 'OK'], [null, 'Fail']])] public function short_ternary($value, $result) { Assert::equals($result, $this->run( - 'class { + 'class %T { public function run($value) { return $value ?: "Fail"; } @@ -44,7 +44,7 @@ public function run($value) { #[Test, Values([[['OK']], [[]]])] public function null_coalesce($value) { Assert::equals('OK', $this->run( - 'class { + 'class %T { public function run($value) { return $value[0] ?? "OK"; } @@ -56,7 +56,7 @@ public function run($value) { #[Test, Values(eval: '[["."], [new Path(".")]]')] public function with_instanceof($value) { Assert::equals(new Path('.'), $this->run( - 'class { + 'class %T { public function run($value) { return $value instanceof \\io\\Path ? $value : new \\io\\Path($value); } diff --git a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php index dfc630e1..03c70fcd 100755 --- a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php @@ -6,49 +6,49 @@ class TrailingCommasTest extends EmittingTest { #[Test] public function in_array() { - $r= $this->run('class { public function run() { return ["test", ]; } }'); + $r= $this->run('class %T { public function run() { return ["test", ]; } }'); Assert::equals(['test'], $r); } #[Test] public function in_map() { - $r= $this->run('class { public function run() { return ["test" => true, ]; } }'); + $r= $this->run('class %T { public function run() { return ["test" => true, ]; } }'); Assert::equals(['test' => true], $r); } #[Test] public function in_function_call() { - $r= $this->run('class { public function run() { return sprintf("Hello %s", "test", ); } }'); + $r= $this->run('class %T { public function run() { return sprintf("Hello %s", "test", ); } }'); Assert::equals('Hello test', $r); } #[Test] public function in_parameter_list() { - $r= $this->run('class { public function run($a, ) { return $a; } }', 'Test'); + $r= $this->run('class %T { public function run($a, ) { return $a; } }', 'Test'); Assert::equals('Test', $r); } #[Test] public function in_isset() { - $r= $this->run('class { public function run() { return isset($a, ); } }'); + $r= $this->run('class %T { public function run() { return isset($a, ); } }'); Assert::equals(false, $r); } #[Test] public function in_list() { - $r= $this->run('class { public function run() { list($a, )= [1, 2]; return $a; } }'); + $r= $this->run('class %T { public function run() { list($a, )= [1, 2]; return $a; } }'); Assert::equals(1, $r); } #[Test] public function in_short_list() { - $r= $this->run('class { public function run() { [$a, ]= [1, 2]; return $a; } }'); + $r= $this->run('class %T { public function run() { [$a, ]= [1, 2]; return $a; } }'); Assert::equals(1, $r); } #[Test] public function in_namespace_group() { - $r= $this->run('use lang\\{Type, }; class { public function run() { return Type::$ARRAY->getName(); } }'); + $r= $this->run('use lang\\{Type, }; class %T { public function run() { return Type::$ARRAY->getName(); } }'); Assert::equals('array', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php index 4f05ab83..a4c156e8 100755 --- a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php @@ -1,6 +1,6 @@ type('class { use \lang\ast\unittest\emit\Loading; }'); - Assert::equals([new XPClass(Loading::class)], $t->getTraits()); + $t= $this->declare('class %T { use \lang\ast\unittest\emit\Loading; }'); + Assert::equals([Reflection::type(Loading::class)], $t->traits()); } #[Test] public function trait_method_is_part_of_type() { - $t= $this->type('class { use \lang\ast\unittest\emit\Loading; }'); - Assert::true($t->hasMethod('loaded')); + $t= $this->declare('class %T { use \lang\ast\unittest\emit\Loading; }'); + Assert::notEquals(null, $t->method('loaded')); } #[Test] public function trait_is_resolved() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading; }'); - Assert::equals([new XPClass(Loading::class)], $t->getTraits()); + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading; }'); + Assert::equals([Reflection::type(Loading::class)], $t->traits()); } #[Test] public function trait_method_aliased() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading { loaded as hasLoaded; } }'); - Assert::true($t->hasMethod('hasLoaded')); + Assert::notEquals(null, $t->method('hasLoaded')); } #[Test] public function trait_method_aliased_qualified() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading { Loading::loaded as hasLoaded; } }'); - Assert::true($t->hasMethod('hasLoaded')); + Assert::notEquals(null, $t->method('hasLoaded')); } #[Test] public function trait_method_insteadof() { - $t= $this->type('use lang\ast\unittest\emit\{Loading, Spinner}; class { + $t= $this->declare('use lang\ast\unittest\emit\{Loading, Spinner}; class %T { use Loading, Spinner { Spinner::loaded as noLongerSpinning; Loading::loaded insteadof Spinner; } }'); $instance= $t->newInstance(); - Assert::equals('Loaded', $t->getMethod('loaded')->invoke($instance)); - Assert::equals('Not spinning', $t->getMethod('noLongerSpinning')->invoke($instance)); + Assert::equals('Loaded', $t->method('loaded')->invoke($instance)); + Assert::equals('Not spinning', $t->method('noLongerSpinning')->invoke($instance)); } #[Test, Runtime(php: '>=8.2.0-dev')] public function can_have_constants() { - $t= $this->type('trait { const FIXTURE = 1; }'); - Assert::equals(1, $t->getConstant('FIXTURE')); + $t= $this->declare('trait %T { const FIXTURE = 1; }'); + Assert::equals(1, $t->constant('FIXTURE')->value()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php index 8dc2f47f..6aadac81 100755 --- a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -38,19 +38,19 @@ public function setUp() { #[Test] public function leaves_class_without_annotations() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $id; public function __construct(int $id) { $this->id= $id; } }'); - Assert::false($t->hasMethod('id')); + Assert::equals(null, $t->method('id')); } #[Test] public function generates_string_representation() { - $t= $this->type('#[Repr] class { + $t= $this->declare('#[Repr] class %T { private int $id; private string $name; @@ -59,16 +59,16 @@ public function __construct(int $id, string $name) { $this->name= $name; } }'); - Assert::true($t->hasMethod('toString')); + Assert::notEquals(null, $t->method('toString')); Assert::equals( "T@[\n id => 1\n name => \"Test\"\n]", - $t->getMethod('toString')->invoke($t->newInstance(1, 'Test')) + $t->method('toString')->invoke($t->newInstance(1, 'Test')) ); } #[Test, Values([['id', 1], ['name', 'Test']])] public function generates_accessor($name, $expected) { - $t= $this->type('#[Getters] class { + $t= $this->declare('#[Getters] class %T { private int $id; private string $name; @@ -77,13 +77,13 @@ public function __construct(int $id, string $name) { $this->name= $name; } }'); - Assert::true($t->hasMethod($name)); - Assert::equals($expected, $t->getMethod($name)->invoke($t->newInstance(1, 'Test'))); + Assert::notEquals(null, $t->method($name)); + Assert::equals($expected, $t->method($name)->invoke($t->newInstance(1, 'Test'))); } #[Test] public function generates_both() { - $t= $this->type('#[Repr, Getters] class { + $t= $this->declare('#[Repr, Getters] class %T { private int $id; private string $name; @@ -94,11 +94,11 @@ public function __construct(int $id, string $name) { }'); $instance= $t->newInstance(1, 'Test'); - Assert::equals(1, $t->getMethod('id')->invoke($instance)); - Assert::equals('Test', $t->getMethod('name')->invoke($instance)); + Assert::equals(1, $t->method('id')->invoke($instance)); + Assert::equals('Test', $t->method('name')->invoke($instance)); Assert::equals( "T@[\n id => 1\n name => \"Test\"\n]", - $t->getMethod('toString')->invoke($instance) + $t->method('toString')->invoke($instance) ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index b88813ac..32401255 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -1,98 +1,110 @@ type($kind.' { }'); + $t= $this->declare($kind.' %T { }'); Assert::equals( - ['const' => [], 'fields' => [], 'methods' => []], - ['const' => $t->getConstants(), 'fields' => $t->getFields(), 'methods' => $t->getMethods()] + ['constants' => [], 'properties' => [], 'methods' => []], + [ + 'constants' => iterator_to_array($t->constants()), + 'properties' => iterator_to_array($t->properties()), + 'methods' => iterator_to_array($t->methods()) + ] ); } #[Test] public function abstract_class_type() { - Assert::true(Modifiers::isAbstract($this->type('abstract class { }')->getModifiers())); + Assert::true($this->declare('abstract class %T { }')->modifiers()->isAbstract()); } #[Test] public function final_class_type() { - Assert::true(Modifiers::isFinal($this->type('final class { }')->getModifiers())); + Assert::true($this->declare('final class %T { }')->modifiers()->isFinal()); } #[Test] public function class_without_parent() { - Assert::null($this->type('class { }')->getParentclass()); + Assert::null($this->declare('class %T { }')->parent()); } #[Test] public function class_with_parent() { Assert::equals( - new XPClass(EmittingTest::class), - $this->type('class extends \\lang\\ast\\unittest\\emit\\EmittingTest { }')->getParentclass() + EmittingTest::class, + $this->declare('class %T extends \\lang\\ast\\unittest\\emit\\EmittingTest { }')->parent()->literal() ); } #[Test] public function trait_type() { - Assert::true($this->type('trait { }')->isTrait()); + Assert::equals(Kind::$TRAIT, $this->declare('trait %T { }')->kind()); } #[Test] public function trait_type_with_method() { - Assert::true($this->type('trait { public function name() { return "Test"; }}')->isTrait()); + Assert::equals( + Kind::$TRAIT, + $this->declare('trait %T { public function name() { return "Test"; }}')->kind() + ); } #[Test] public function interface_type() { - Assert::true($this->type('interface { }')->isInterface()); + Assert::equals(Kind::$INTERFACE, $this->declare('interface %T { }')->kind()); } #[Test] public function interface_type_with_method() { - Assert::true($this->type('interface { public function name(); }')->isInterface()); + Assert::equals( + Kind::$INTERFACE, + $this->declare('interface %T { public function name(); }')->kind() + ); } - #[Test, Values(['public', 'private', 'protected'])] + #[Test, Values(['public', 'private', 'protected']), Runtime(php: '>=7.1.0')] public function constant($modifiers) { - $c= $this->type('class { '.$modifiers.' const test = 1; }')->getConstant('test'); - Assert::equals(1, $c); + $c= $this->declare('class %T { '.$modifiers.' const test = 1; }')->constant('test'); + Assert::equals( + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $c->name(), 'type' => $c->constraint()->type(), 'modifiers' => $c->modifiers()->names()] + ); } #[Test, Values(['public', 'private', 'protected', 'public static', 'private static', 'protected static'])] - public function field($modifiers) { - $f= $this->type('class { '.$modifiers.' $test; }')->getField('test'); - $n= implode(' ', Modifiers::namesOf($f->getModifiers())); + public function property($modifiers) { + $p= $this->declare('class %T { '.$modifiers.' $test; }')->property('test'); Assert::equals( - ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], - ['name' => $f->getName(), 'type' => $f->getTypeName(), 'modifiers' => $n] + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $p->name(), 'type' => $p->constraint()->type(), 'modifiers' => $p->modifiers()->names()] ); } #[Test, Values(['public', 'protected', 'private', 'public final', 'protected final', 'public static', 'protected static', 'private static'])] public function method($modifiers) { - $m= $this->type('class { '.$modifiers.' function test() { } }')->getMethod('test'); - $n= implode(' ', Modifiers::namesOf($m->getModifiers())); + $m= $this->declare('class %T { '.$modifiers.' function test() { } }')->method('test'); Assert::equals( - ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], - ['name' => $m->getName(), 'type' => $m->getReturnTypeName(), 'modifiers' => $n] + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $m->name(), 'type' => $m->returns()->type(), 'modifiers' => $m->modifiers()->names()] ); } #[Test] public function abstract_method() { - $m= $this->type('abstract class { abstract function test(); }')->getMethod('test'); - Assert::true(Modifiers::isAbstract($m->getModifiers())); + $m= $this->declare('abstract class %T { abstract function test(); }')->method('test'); + Assert::true($m->modifiers()->isAbstract()); } #[Test] public function method_with_keyword() { - $t= $this->type('class { + $t= $this->declare('class %T { private $items; public static function new($items) { @@ -109,6 +121,6 @@ public static function run($values) { return self::new($values)->forEach(function($a) { return $a * 2; }); } }'); - Assert::equals([2, 4, 6], $t->getMethod('run')->invoke(null, [[1, 2, 3]])); + Assert::equals([2, 4, 6], $t->method('run')->invoke(null, [[1, 2, 3]])); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php index 2ade1e33..c03b42e4 100755 --- a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php @@ -6,7 +6,7 @@ class UnicodeEscapesTest extends EmittingTest { #[Test] public function spanish_ñ() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return "ma\u{00F1}ana"; } @@ -17,7 +17,7 @@ public function run() { #[Test] public function emoji() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return "Smile! \u{1F602}"; } diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index ea803453..f0509784 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -13,109 +13,109 @@ class UnionTypesTest extends EmittingTest { #[Test] public function field_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private int|string $test; }'); Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } #[Test] public function parameter_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(int|string $arg) { } }'); Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getMethod('test')->getParameter(0)->getType() + $t->method('test')->parameter(0)->constraint()->type() ); } #[Test] public function return_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): int|string { } }'); Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getMethod('test')->getReturnType() + $t->method('test')->returns()->type() ); } #[Test] public function nullable_union_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): int|string|null { } }'); Assert::equals( new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), - $t->getMethod('test')->getReturnType() + $t->method('test')->returns()->type() ); } #[Test] public function nullable_union_type_alternative_syntax() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): ?(int|string) { } }'); Assert::equals( new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), - $t->getMethod('test')->getReturnType() + $t->method('test')->returns()->type() ); } #[Test, Runtime(php: '>=8.0.0-dev')] public function nullable_union_type_restriction() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): int|string|null { } }'); Assert::equals( new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), - $t->getMethod('test')->getReturnTypeRestriction() + $t->method('test')->returns()->type() ); } #[Test, Runtime(php: '>=8.0.0-dev')] public function parameter_type_restriction_with_php8() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(int|string|array $arg) { } }'); Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), - $t->getMethod('test')->getParameter(0)->getTypeRestriction() + $t->method('test')->parameter(0)->constraint()->type() ); } #[Test, Runtime(php: '>=8.0.0-dev')] public function parameter_function_type_restriction_with_php8() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): string|(function(): string) { } }'); Assert::equals( new TypeUnion([Primitive::$STRING, Type::$CALLABLE]), - $t->getMethod('test')->getReturnTypeRestriction() + $t->method('test')->returns()->type() ); } #[Test, Runtime(php: '>=8.0.0-dev')] public function return_type_restriction_with_php8() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): int|string|array { } }'); Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), - $t->getMethod('test')->getReturnTypeRestriction() + $t->method('test')->returns()->type() ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index fd382d29..5652b3f5 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -11,7 +11,7 @@ class UsingTest extends EmittingTest { #[Test] public function dispose_called() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -27,7 +27,7 @@ public function run() { #[Test] public function dispose_called_for_all() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -43,7 +43,7 @@ public function run() { #[Test] public function dispose_called_even_when_exceptions_occur() { - $r= $this->run('use lang\{IllegalArgumentException, IllegalStateException}; use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\{IllegalArgumentException, IllegalStateException}; use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -63,7 +63,7 @@ public function run() { #[Test] public function supports_closeables() { - $r= $this->run('use lang\ast\unittest\emit\FileInput; class { + $r= $this->run('use lang\ast\unittest\emit\FileInput; class %T { public function run() { FileInput::$open= false; @@ -79,7 +79,7 @@ public function run() { #[Test] public function can_return_from_inside_using() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private function read() { using ($x= new Handle(1)) { return $x->read(); @@ -97,7 +97,7 @@ public function run() { #[Test] public function variable_undefined_after_using() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { using ($x= new Handle(1)) { // NOOP @@ -110,7 +110,7 @@ public function run() { #[Test] public function variable_undefined_after_using_even_if_previously_defined() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { $x= new Handle(1); using ($x) { diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index 6c0419d2..f64b6c96 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -6,7 +6,7 @@ class VarargsTest extends EmittingTest { #[Test] public function vsprintf() { - $r= $this->run('class { + $r= $this->run('class %T { private function format(string $format, ... $args) { return vsprintf($format, $args); } @@ -21,7 +21,7 @@ public function run() { #[Test] public function list_of() { - $r= $this->run('class { + $r= $this->run('class %T { private function listOf(string... $args) { return $args; } diff --git a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php index 34537f18..9f39d459 100755 --- a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php @@ -11,25 +11,25 @@ protected function emitters() { return [XpMeta::class, VirtualPropertyTypes::cla #[Test] public function type_available_via_reflection() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $value; }'); - Assert::equals(Primitive::$INT, $t->getField('value')->getType()); + Assert::equals(Primitive::$INT, $t->property('value')->constraint()->type()); } #[Test] public function modifiers_available_via_reflection() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $value; }'); - Assert::equals(MODIFIER_PRIVATE, $t->getField('value')->getModifiers()); + Assert::equals(MODIFIER_PRIVATE, $t->property('value')->modifiers()->bits()); } #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] public function cannot_read_private_field() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $value; }'); @@ -38,7 +38,7 @@ public function cannot_read_private_field() { #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] public function cannot_write_private_field() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $value; }'); @@ -47,7 +47,7 @@ public function cannot_write_private_field() { #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] public function cannot_read_protected_field() { - $t= $this->type('class { + $t= $this->declare('class %T { protected int $value; }'); @@ -56,7 +56,7 @@ public function cannot_read_protected_field() { #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] public function cannot_write_protected_field() { - $t= $this->type('class { + $t= $this->declare('class %T { protected int $value; }'); @@ -65,10 +65,10 @@ public function cannot_write_protected_field() { #[Test] public function can_access_protected_field_from_subclass() { - $t= $this->type('class { + $t= $this->declare('class %T { protected int $value; }'); - $i= newinstance($t->getName(), [], [ + $i= newinstance($t->literal(), [], [ 'run' => function() { $this->value= 6100; return $this->value; @@ -80,16 +80,16 @@ public function can_access_protected_field_from_subclass() { #[Test] public function initial_value_available_via_reflection() { - $t= $this->type('class { + $t= $this->declare('class %T { private int $value = 6100; }'); - Assert::equals(6100, $t->getField('value')->setAccessible(true)->get($t->newInstance())); + Assert::equals(6100, $t->property('value')->get($t->newInstance(), $t)); } #[Test, Values([[null], ['Test'], [[]]]), Expect(class: Error::class, message: '/property .+::\$value of type int/')] public function type_checked_at_runtime($in) { - $this->run('class { + $this->run('class %T { private int $value; public function run($arg) { @@ -101,7 +101,7 @@ public function run($arg) { #[Test] public function value_type_test() { $handle= new Handle(0); - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private Handle $value; public function run($arg) { @@ -115,7 +115,7 @@ public function run($arg) { #[Test, Values(['', 'Test', 1, 1.5, true, false])] public function string_type_coercion($in) { - $r= $this->run('class { + $r= $this->run('class %T { private string $value; public function run($arg) { @@ -129,7 +129,7 @@ public function run($arg) { #[Test, Values(['', 'Test', 1, 1.5, true, false])] public function bool_type_coercion($in) { - $r= $this->run('class { + $r= $this->run('class %T { private bool $value; public function run($arg) { @@ -143,7 +143,7 @@ public function run($arg) { #[Test, Values(['1', '1.5', 1, 1.5, true, false])] public function int_type_coercion($in) { - $r= $this->run('class { + $r= $this->run('class %T { private int $value; public function run($arg) { @@ -157,7 +157,7 @@ public function run($arg) { #[Test, Values(['1', '1.5', 1, 1.5, true, false])] public function float_type_coercion($in) { - $r= $this->run('class { + $r= $this->run('class %T { private float $value; public function run($arg) { diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index 6214402c..40e2917b 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -7,7 +7,7 @@ class YieldTest extends EmittingTest { #[Test] public function yield_without_argument() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield; yield; @@ -18,7 +18,7 @@ public function run() { #[Test] public function yield_values() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield 1; yield 2; @@ -30,7 +30,7 @@ public function run() { #[Test] public function yield_keys_and_values() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield "color" => "orange"; yield "price" => 12.99; @@ -41,7 +41,7 @@ public function run() { #[Test] public function yield_from_array() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield from [1, 2, 3]; } @@ -51,7 +51,7 @@ public function run() { #[Test] public function yield_from_generator() { - $r= $this->run('class { + $r= $this->run('class %T { private function values() { yield 1; yield 2; @@ -67,7 +67,7 @@ public function run() { #[Test] public function yield_from_and_yield() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield 1; yield from [2, 3]; @@ -79,7 +79,7 @@ public function run() { #[Test] public function yield_send() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { while ($line= yield) { echo $line, "\n"; From 8d539f5824bf8b144576b9567485026f6651deae Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 13:06:06 +0200 Subject: [PATCH 756/926] Extract commonly used method to trait --- .../unittest/emit/AnnotationSupport.class.php | 15 +-------------- .../ast/unittest/emit/AnnotationsOf.class.php | 18 ++++++++++++++++++ .../lang/ast/unittest/emit/EnumTest.class.php | 15 +-------------- .../ast/unittest/emit/ParameterTest.class.php | 16 +--------------- 4 files changed, 21 insertions(+), 43 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index b46612e4..fd566b36 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -10,20 +10,7 @@ * - AttributesTest - emits PHP 8 attributes */ abstract class AnnotationSupport extends EmittingTest { - - /** - * Returns annotations present in the given type - * - * @param lang.reflection.Annotated $annotated - * @return [:var[]] - */ - private function annotations($annotated) { - $r= []; - foreach ($annotated->annotations() as $name => $annotation) { - $r[$name]= $annotation->arguments(); - } - return $r; - } + use AnnotationsOf; #[Test] public function without_value() { diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php new file mode 100755 index 00000000..cda1b705 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php @@ -0,0 +1,18 @@ +annotations() as $name => $annotation) { + $r[$name]= $annotation->arguments(); + } + return $r; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 7fa73299..eec53f11 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -7,20 +7,7 @@ #[Condition(assert: 'function_exists("enum_exists")')] class EnumTest extends EmittingTest { - - /** - * Returns annotations present in the given type - * - * @param lang.reflection.Annotated $annotated - * @return [:var[]] - */ - private function annotations($annotated) { - $r= []; - foreach ($annotated->annotations() as $name => $annotation) { - $r[$name]= $annotation->arguments(); - } - return $r; - } + use AnnotationsOf; #[Test] public function enum_type() { diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index aebf93b0..f2fc7f1d 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -5,21 +5,7 @@ use test\{Action, Assert, Test, Values}; class ParameterTest extends EmittingTest { - use NullableSupport; - - /** - * Returns annotations present in the given type - * - * @param lang.reflection.Annotated $annotated - * @return [:var[]] - */ - private function annotations($annotated) { - $r= []; - foreach ($annotated->annotations() as $name => $annotation) { - $r[$name]= $annotation->arguments(); - } - return $r; - } + use AnnotationsOf, NullableSupport; /** * Helper to declare a type and return a parameter reflection object From 474c49a6a795b920d59244c8e2d745b4fbe72f9a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 4 Jun 2023 13:23:21 +0200 Subject: [PATCH 757/926] Test enum constant annotations for PHP 8.1+ --- src/main/php/lang/ast/emit/PHP.class.php | 9 +++++++++ src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 26ad1bef..e0faddb4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -376,6 +376,15 @@ protected function emitLambda($result, $lambda) { } protected function emitEnumCase($result, $case) { + $result->codegen->scope[0]->meta[self::CONSTANT][$case->name]= [ + DETAIL_RETURNS => 'self', + DETAIL_ANNOTATIONS => $case->annotations, + DETAIL_COMMENT => $case->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $case->annotations && $this->emitOne($result, $case->annotations); $result->out->write('case '.$case->name); if ($case->expression) { $result->out->write('='); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index eec53f11..36a9ace1 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -2,8 +2,8 @@ use lang\reflection\{Kind, InvocationFailed}; use lang\{Enum, Error}; -use test\verify\Condition; -use test\{Action, Assert, Expect, Ignore, Test, Values}; +use test\verify\{Condition, Runtime}; +use test\{Action, Assert, Expect, Test, Values}; #[Condition(assert: 'function_exists("enum_exists")')] class EnumTest extends EmittingTest { @@ -207,7 +207,7 @@ public function enum_annotations() { Assert::equals(['Test' => []], $this->annotations($t)); } - #[Test, Ignore('XP reflection does not support constant annotations')] + #[Test, Runtime(php: '>=8.1')] public function enum_member_annotations() { $t= $this->declare('enum %T { #[Test] case ONE; }'); Assert::equals(['Test' => []], $this->annotations($t->constant('ONE'))); From 029d2ff4cd16927711a01f3f78a89a050db515a5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jun 2023 21:06:40 +0200 Subject: [PATCH 758/926] Restore backwards compatibility for test infrastructure --- .../ast/unittest/emit/EmittingTest.class.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index 42ca24cb..a565c35a 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -69,6 +69,17 @@ protected function emit($code) { return $out->bytes(); } + /** + * Declare a type with a unique type name (which may be referenced by `%T`) + * and return a type referencing it. + * + * @param string $code + * @return lang.XPClass + */ + protected function type($code) { + return $this->declare($code)->class(); + } + /** * Declare a type with a unique type name (which may be referenced by `%T`) * and return a reflection instance referencing it. @@ -78,10 +89,13 @@ protected function emit($code) { */ protected function declare($code) { $name= 'T'.(self::$id++); - $declaration= strstr($code, '%T') - ? str_replace('%T', $name, $code) - : $code.' class '.$name.' { }' - ; + if (strstr($code, '%T')) { + $declaration= str_replace('%T', $name, $code); + } else if (strstr($code, '')) { + $declaration= str_replace('', $name, $code); // deprecated + } else { + $declaration= $code.' class '.$name.' { }'; + } $tree= $this->language->parse(new Tokens($declaration, static::class))->tree(); if (isset($this->output['ast'])) { From 5aa77806b0470d071788e05da82269041f55edc9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jun 2023 21:12:33 +0200 Subject: [PATCH 759/926] 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 b2caa53a0848fea3dc31bd1f21ebfb3b4cf1e0b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 8 Jun 2023 13:34:42 +0200 Subject: [PATCH 760/926] Emit arbitrary static variable initializers for PHP 8.3+ Implements #162 --- ChangeLog.md | 4 +++ src/main/php/lang/ast/emit/PHP.class.php | 9 ++----- src/main/php/lang/ast/emit/PHP70.class.php | 1 + src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + src/main/php/lang/ast/emit/PHP73.class.php | 1 + src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 3 ++- src/main/php/lang/ast/emit/PHP81.class.php | 8 +++++- src/main/php/lang/ast/emit/PHP82.class.php | 8 +++++- ...iteStaticVariableInitializations.class.php | 26 +++++++++++++++++++ 11 files changed, 53 insertions(+), 10 deletions(-) create mode 100755 src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 2e1e3ec5..65516620 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Implemented feature #162: Arbitrary static variable initializers, see + https://wiki.php.net/rfc/arbitrary_static_variable_initializers + (@thekid) + ## 8.13.0 / 2023-06-04 * Merged PR #169: Refactor how annotations with non-constant arguments diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index e0faddb4..753a6a00 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -233,13 +233,8 @@ protected function emitStatic($result, $static) { foreach ($static->initializations as $variable => $initial) { $result->out->write('static $'.$variable); if ($initial) { - if ($this->isConstant($result, $initial)) { - $result->out->write('='); - $this->emitOne($result, $initial); - } else { - $result->out->write('= null; null === $'.$variable.' && $'.$variable.'= '); - $this->emitOne($result, $initial); - } + $result->out->write('='); + $this->emitOne($result, $initial); } $result->out->write(';'); } diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 84c55b61..49b90231 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -29,6 +29,7 @@ class PHP70 extends PHP { RewriteExplicitOctals, RewriteLambdaExpressions, RewriteMultiCatch, + RewriteStaticVariableInitializations, 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..ecf1ea1c 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -27,6 +27,7 @@ class PHP71 extends PHP { RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteStaticVariableInitializations, 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..21ef7b23 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -27,6 +27,7 @@ class PHP72 extends PHP { RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteStaticVariableInitializations, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index 820b6e99..8b0c0fd9 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -27,6 +27,7 @@ class PHP73 extends PHP { RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteStaticVariableInitializations, 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..77fe94a1 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -24,6 +24,7 @@ class PHP74 extends PHP { RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, + RewriteStaticVariableInitializations, 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..48f37fd0 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -27,7 +27,8 @@ class PHP80 extends PHP { RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteEnums, - RewriteExplicitOctals + RewriteExplicitOctals, + RewriteStaticVariableInitializations ; /** 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..ce52501f 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -19,7 +19,13 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; + use + RewriteBlockLambdaExpressions, + RewriteDynamicClassConstants, + RewriteStaticVariableInitializations, + ReadonlyClasses, + OmitConstantTypes + ; /** 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..754306f1 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -19,7 +19,13 @@ * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { - use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; + use + RewriteBlockLambdaExpressions, + RewriteDynamicClassConstants, + RewriteStaticVariableInitializations, + ReadonlyClasses, + OmitConstantTypes + ; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php b/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php new file mode 100755 index 00000000..618fe57c --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php @@ -0,0 +1,26 @@ +initializations as $variable => $initial) { + $result->out->write('static $'.$variable); + if ($initial) { + if ($this->isConstant($result, $initial)) { + $result->out->write('='); + $this->emitOne($result, $initial); + } else { + $result->out->write('= null; null === $'.$variable.' && $'.$variable.'= '); + $this->emitOne($result, $initial); + } + } + $result->out->write(';'); + } + } +} \ No newline at end of file From eaf9dc708137e26013e7dedbfb9eec88ae7c02f9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Jun 2023 13:20:48 +0200 Subject: [PATCH 761/926] Add various type checking tests --- .../unittest/emit/TypeCheckingTest.class.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php new file mode 100755 index 00000000..5257ed34 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php @@ -0,0 +1,77 @@ +run('class %T { + public function run(int $param) { + return $param; + } + }', $param); + Assert::equals((int)$param, $r); + } + + #[Test, Values(['', 'Test', 1, 1.5, true, false])] + public function string_type_coerces($param) { + $r= $this->run('class %T { + public function run(string $param) { + return $param; + } + }', $param); + Assert::equals((string)$param, $r); + } + + #[Test, Values([null, [[]]]), Expect(Error::class)] + public function string_type_rejects($param) { + $this->run('class %T { + public function run(string $param) { + return $param; + } + }', $param); + } + + #[Test, Values(eval: '[[new Date(), new Bytes("test"), null]]')] + public function nullable_value_type_accepts($param) { + $r= $this->run('use lang\\Value; class %T { + public function run(?Value $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values(eval: '[["test", new Bytes("test")]]')] + public function union_type_accepts($param) { + $r= $this->run('class %T { + public function run(string|Bytes $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values(eval: '[[new Bytes("test")]]')] + public function intersection_type_accepts($param) { + $r= $this->run('use lang\\Value; class %T { + public function run(Value&Traversable $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values([[[]], [['a', 'b', 'c']]])] + public function string_array_type_accepts($param) { + $r= $this->run('class %T { + public function run(array $param) { + return $param; + } + }', $param); + Assert::equals((array)$param, $r); + } +} \ No newline at end of file From 897e021bbddd50510b8562237c1382d99ce29dcc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 11:36:35 +0200 Subject: [PATCH 762/926] Use uppercase for `Default` annotation --- src/main/php/lang/ast/emit/PHP.class.php | 4 ++-- .../InitializeWithExpressionsTest.class.php | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 753a6a00..381b67b4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -640,9 +640,9 @@ protected function emitMethod($result, $method) { foreach ($method->signature->parameters as $param) { if (isset($param->promote)) $promoted[]= $param; - // Create a parameter annotation named `default` for non-constant parameter defaults + // Create a parameter annotation named `Default` for non-constant parameter defaults if (isset($param->default) && !$this->isConstant($result, $param->default)) { - $param->annotate(new Annotation('default', [$param->default])); + $param->annotate(new Annotation('Default', [$param->default])); $init[]= $param; } diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index f7eca64d..940bca5c 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -1,6 +1,6 @@ run(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { + $t= $this->declare(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { const INITIAL= "initial"; - private $h= %D; + public $h= %D; + }', ['%D' => $declaration])); - public function run() { - return typeof($this)->getField("h")->get($this); - } - }', ['%D' => $declaration]))); + Assert::equals($expected, $t->property('h')->get($t->newInstance())); } #[Test, Values(['fn($arg) => $arg->redirect(1)', 'function($arg) { return $arg->redirect(1); }'])] @@ -151,12 +149,12 @@ public function run($h= new Handle(0)) { #[Test] public function parameter_default_reflective_access() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + $t= $this->declare('use lang\ast\unittest\emit\Handle; class %T { public function run($h= new Handle(0)) { - return typeof($this)->getMethod("run")->getParameter(0)->getDefaultValue(); + // NOOP } }'); - Assert::equals(new Handle(0), $r); + Assert::equals(new Handle(0), $t->method('run')->parameter(0)->default()); } #[Test] From d97850c6528a426ce2849b39de0febf8c6d457c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 11:40:12 +0200 Subject: [PATCH 763/926] Optimize emitted code for non-constant parameter defaults --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 381b67b4..0ddb234a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -658,7 +658,7 @@ protected function emitMethod($result, $method) { // Emit non-constant parameter defaults foreach ($init as $param) { - $result->out->write('null === $'.$param->name.' && $'.$param->name.'='); + $result->out->write('$'.$param->name.' ?? $'.$param->name.'='); $this->emitOne($result, $param->default); $result->out->write(';'); } From e2d056ac73dace878436f3a860e59e5de29146b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 11:50:58 +0200 Subject: [PATCH 764/926] Release 8.14.0 --- ChangeLog.md | 5 +++++ src/main/php/lang/ast/emit/PHP82.class.php | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 65516620..6610c844 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.14.0 / 2023-07-15 + +* Fixed error *Cannot access offset of type array on array* when using + reflection for non-constant parameter defaults + (@thekid) * Implemented feature #162: Arbitrary static variable initializers, see https://wiki.php.net/rfc/arbitrary_static_variable_initializers (@thekid) diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 754306f1..5e508762 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -20,10 +20,10 @@ */ class PHP82 extends PHP { use - RewriteBlockLambdaExpressions, - RewriteDynamicClassConstants, - RewriteStaticVariableInitializations, - ReadonlyClasses, + RewriteBlockLambdaExpressions, + RewriteDynamicClassConstants, + RewriteStaticVariableInitializations, + ReadonlyClasses, OmitConstantTypes ; From e428a1cc88c680a7bf07c881247ae0d80a76d196 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 12:33:11 +0200 Subject: [PATCH 765/926] Refactor scope lookup from Result to CodeGen This means we can use it inside transformations --- src/main/php/lang/ast/CodeGen.class.php | 30 +++++++++++++++++++ .../php/lang/ast/emit/Declaration.class.php | 10 ++----- .../php/lang/ast/emit/GeneratedCode.class.php | 21 ++----------- src/main/php/lang/ast/emit/PHP.class.php | 4 +-- .../unittest/emit/GeneratedCodeTest.class.php | 4 +-- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index 439a1957..8066878a 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -1,5 +1,7 @@ scope[0] ?? null; + + if ('self' === $type || 'static' === $type) { + return new Declaration($enclosing->type); + } else if ('parent' === $type) { + return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null; + } + + foreach ($this->scope as $scope) { + if ($scope->type->name && $type === $scope->type->name->literal()) { + return new Declaration($scope->type); + } + } + + if (class_exists($type) || interface_exists($type) || trait_exists($type) || enum_exists($type)) { + return new Reflection($type); + } else { + return new Incomplete($type); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index f3d58a3a..82240a57 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -3,17 +3,13 @@ use lang\ast\nodes\{EnumCase, Property}; class Declaration extends Type { - private $type, $result; + private $type; static function __static() { } - /** - * @param lang.ast.nodes.TypeDeclaration $type - * @param lang.ast.Result $result - */ - public function __construct($type, $result) { + /** @param lang.ast.nodes.TypeDeclaration $type */ + public function __construct($type) { $this->type= $type; - $this->result= $result; } /** @return string */ diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 9c1716af..e02acbe9 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -62,28 +62,11 @@ public function temp() { /** * Looks up a given type * + * @deprecated Use `CodeGen::lookup()` instead! * @param string $type * @return lang.ast.emit.Type */ public function lookup($type) { - $enclosing= $this->codegen->scope[0] ?? null; - - if ('self' === $type || 'static' === $type) { - return new Declaration($enclosing->type, $this); - } else if ('parent' === $type) { - return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null; - } - - foreach ($this->codegen->scope as $scope) { - if ($scope->type->name && $type === $scope->type->name->literal()) { - return new Declaration($scope->type, $this); - } - } - - if (class_exists($type) || interface_exists($type) || trait_exists($type) || enum_exists($type)) { - return new Reflection($type); - } else { - return new Incomplete($type); - } + return $this->codegen->lookup($type); } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0ddb234a..1f6ffb34 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -70,7 +70,7 @@ protected function isConstant($result, $node) { return ( $node->member instanceof Literal && is_string($node->type) && - !$result->lookup($node->type)->rewriteEnumCase($node->member->expression) + !$result->codegen->lookup($node->type)->rewriteEnumCase($node->member->expression) ); } else if ($node instanceof BinaryExpression) { return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); @@ -1064,7 +1064,7 @@ protected function emitScope($result, $scope) { $scope->member instanceof Literal && is_string($scope->type) && 'class' !== $scope->member->expression && - $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression) + $result->codegen->lookup($scope->type)->rewriteEnumCase($scope->member->expression) ) { $result->out->write('$'.$scope->member->expression); } else { diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 87a9ff3a..a5fc9402 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -49,7 +49,7 @@ public function lookup_self() { $r= new GeneratedCode(new MemoryOutputStream()); $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($context->type, $r), $r->lookup('self')); + Assert::equals(new Declaration($context->type), $r->lookup('self')); } #[Test] @@ -73,7 +73,7 @@ public function lookup_named() { $r= new GeneratedCode(new MemoryOutputStream()); $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($context->type, $r), $r->lookup('\\T')); + Assert::equals(new Declaration($context->type), $r->lookup('\\T')); } #[Test] From 24f63f69770c2ed88886227fc09e57710ecb039f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 13:11:34 +0200 Subject: [PATCH 766/926] Document scope lookup API change --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 6610c844..78636ead 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #171: Refactor scope lookup from Result to CodeGen - @thekid + ## 8.14.0 / 2023-07-15 * Fixed error *Cannot access offset of type array on array* when using From 923ea6417f4b5049460bcc03e6058d37c3355a6d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 17:17:46 +0200 Subject: [PATCH 767/926] Check for `#[Override]` annotation See https://wiki.php.net/rfc/marking_overriden_methods --- src/main/php/lang/ast/CodeGen.class.php | 1 + src/main/php/lang/ast/Emitter.class.php | 8 ++- .../php/lang/ast/emit/CheckOverride.class.php | 38 ++++++++++++ .../php/lang/ast/emit/Declaration.class.php | 17 ++++++ .../php/lang/ast/emit/Incomplete.class.php | 13 ++++ src/main/php/lang/ast/emit/PHP70.class.php | 1 + src/main/php/lang/ast/emit/PHP71.class.php | 1 + src/main/php/lang/ast/emit/PHP72.class.php | 1 + src/main/php/lang/ast/emit/PHP73.class.php | 1 + src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 1 + src/main/php/lang/ast/emit/PHP81.class.php | 1 + src/main/php/lang/ast/emit/PHP82.class.php | 1 + .../php/lang/ast/emit/Reflection.class.php | 13 ++++ src/main/php/lang/ast/emit/Result.class.php | 12 ++++ src/main/php/lang/ast/emit/Type.class.php | 11 ++++ .../php/xp/compiler/CompileRunner.class.php | 3 +- .../emit/MethodOverridingTest.class.php | 59 +++++++++++++++++++ 18 files changed, 180 insertions(+), 3 deletions(-) create mode 100755 src/main/php/lang/ast/emit/CheckOverride.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index 8066878a..b0e82cce 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -5,6 +5,7 @@ class CodeGen { private $id= 0; public $scope= []; + public $source= '(unknown)'; /** Creates a new, unique symbol */ public function symbol() { return '_'.($this->id++); } diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 912e488a..683e04d0 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -173,16 +173,20 @@ protected abstract function result($target); /** * Emitter entry point, takes nodes and emits them to the given target. * + * * @param iterable $nodes * @param io.streams.OutputStream $target + * @param ?string $source * @return io.streams.OutputStream * @throws lang.ast.Errors */ - public function write($nodes, OutputStream $target) { - $result= $this->result($target); + public function write($nodes, OutputStream $target, $source= null) { + $result= $this->result($target)->from($source); try { $this->emitAll($result, $nodes); return $target; + } catch (Error $e) { + throw new Errors([$e], $source); } finally { $result->close(); } diff --git a/src/main/php/lang/ast/emit/CheckOverride.class.php b/src/main/php/lang/ast/emit/CheckOverride.class.php new file mode 100755 index 00000000..9ed8abdc --- /dev/null +++ b/src/main/php/lang/ast/emit/CheckOverride.class.php @@ -0,0 +1,38 @@ +annotations && $method->annotations->named(Override::class)) { + + // Check parent class + if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod($method->name)) goto emit; + + // Check all implemented interfaces + foreach ($result->codegen->lookup('self')->implementedInterfaces() as $interface) { + if ($result->codegen->lookup($interface)->providesMethod($method->name)) goto emit; + } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + substr($result->codegen->scope[0]->type->name, 1), + $method->name + ), + $result->codegen->source, + $method->line + ); + } + + emit: parent::emitMethod($result, $method); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 82240a57..44c617bc 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -15,6 +15,23 @@ public function __construct($type) { /** @return string */ public function name() { return ltrim($this->type->name, '\\'); } + /** @return iterable */ + public function implementedInterfaces() { + foreach ($this->type->implements as $interface) { + yield $interface->literal(); + } + } + + /** + * Checks whether a given method exists + * + * @param string $named + * @return bool + */ + public function providesMethod($named) { + return isset($this->body["{$named}()"]); + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php index f4fcaffb..fb4d164f 100755 --- a/src/main/php/lang/ast/emit/Incomplete.class.php +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -9,6 +9,19 @@ public function __construct($name) { $this->name= $name; } /** @return string */ public function name() { return $this->name; } + /** @return iterable */ + public function implementedInterfaces() { return []; } + + /** + * Checks whether a given method exists + * + * @param string $named + * @return bool + */ + public function providesMethod($named) { + return false; + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 49b90231..3a18ed07 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -14,6 +14,7 @@ class PHP70 extends PHP { ArbitrayNewExpressions, ArrayUnpackUsingMerge, AttributesAsComments, + CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index ecf1ea1c..b99a76da 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -13,6 +13,7 @@ class PHP71 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 21ef7b23..36756e5e 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -13,6 +13,7 @@ class PHP72 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index 8b0c0fd9..123a569f 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -15,6 +15,7 @@ class PHP73 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + CheckOverride, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 77fe94a1..c6087fc3 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, + CheckOverride, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 48f37fd0..34ea170c 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -21,6 +21,7 @@ class PHP80 extends PHP { use ArrayUnpackUsingMerge, CallablesAsClosures, + CheckOverride, OmitConstantTypes, ReadonlyClasses, ReadonlyProperties, diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index ce52501f..cdc07916 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -20,6 +20,7 @@ */ class PHP81 extends PHP { use + CheckOverride, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 5e508762..708eea1b 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -20,6 +20,7 @@ */ class PHP82 extends PHP { use + CheckOverride, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 858439b4..d1bc46c8 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -23,6 +23,19 @@ public function __construct($type) { /** @return string */ public function name() { return $this->reflect->name; } + /** @return iterable */ + public function implementedInterfaces() { return $this->type->getInterfaceNames(); } + + /** + * Checks whether a given method exists + * + * @param string $named + * @return bool + */ + public function providesMethod($named) { + return $this->reflect->hasMethod($named); + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php index bd034524..f83146c0 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -21,6 +21,18 @@ public function __construct(OutputStream $out) { $this->initialize(); } + /** + * Set filename this result originates from, defaulting to `(unknown)`. + * + * @param ?string $file + * @return self + */ + public function from($file) { + $this->codegen->source= $file ?? '(unknown)'; + return $this; + } + + /** * Initialize result. Guaranteed to be called *once* from constructor. * Without implementation here - overwrite in subclasses. diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index be458366..1c5ff645 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -11,6 +11,17 @@ static function __static() { /** @return string */ public abstract function name(); + /** @return iterable */ + public abstract function implementedInterfaces(); + + /** + * Checks whether a given method exists + * + * @param string $named + * @return bool + */ + public abstract function providesMethod($named); + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 6f3fb3e4..aee9e14a 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -113,7 +113,8 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $emit->write($lang->parse(new Tokens($source, $file))->stream(), $output->target((string)$path)); + $parse= $lang->parse(new Tokens($source, $file)); + $emit->write($parse->stream(), $output->target((string)$path), $parse->file); $t->stop(); $quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php new file mode 100755 index 00000000..fef91974 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -0,0 +1,59 @@ +declare($code); + return null; + } catch (Error $e) { + return preg_replace('/T[0-9]+/', '%T', $e->getMessage()); + } + } + + #[Test] + public function without_annotations() { + Assert::null($this->verify('class %T { public function fixture() { } }')); + } + + #[Test] + public function correctly_overwriting_parent_method() { + Assert::null($this->verify('class %T extends \lang\Throwable { + #[Override] + public function compoundMessage() { } + }')); + } + + #[Test] + public function correctly_implementing_interface_method() { + Assert::null($this->verify('class %T implements \lang\Runnable { + #[Override] + public function run() { } + }')); + } + + #[Test] + public function without_parent() { + Assert::equals( + '%T::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T { + #[Override] + public function fixture() { } + }' + )); + } + + #[Test] + public function overriding_non_existant_method() { + Assert::equals( + '%T::nonExistant() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T extends \lang\Throwable { + #[Override] + public function nonExistant() { } + }' + )); + } +} \ No newline at end of file From 514de5d3409188d498d7ea2c5e28752d0d94922e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 17:38:19 +0200 Subject: [PATCH 768/926] Run code in new runtime for PHP 8.3 --- .../emit/MethodOverridingTest.class.php | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index fef91974..cc66790f 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -1,16 +1,42 @@ = 8.3: PHP will raise errors for us + if (PHP_VERSION_ID >= 80300) { + $rt= Runtime::getInstance()->newInstance(null, 'class', 'xp.runtime.Evaluate', [str_replace('%T', 'T', $code)]); + $rt->in->close(); + $err= $rt->err->readLine(); + $rt->close(); + + return false === $err ? null : preg_replace('/Uncaught error: Compile error \((.+)\)/', '$1', $err); + } + + // PHP < 8.3: XP compiler will raise errors for us try { - $this->declare($code); + $t= $this->declare($code); return null; } catch (Error $e) { - return preg_replace('/T[0-9]+/', '%T', $e->getMessage()); + return preg_replace('/T[0-9]+/', 'T', $e->getMessage()); } } @@ -38,7 +64,7 @@ public function run() { } #[Test] public function without_parent() { Assert::equals( - '%T::fixture() has #[\Override] attribute, but no matching parent method exists', + 'T::fixture() has #[\Override] attribute, but no matching parent method exists', $this->verify('class %T { #[Override] public function fixture() { } @@ -49,7 +75,7 @@ public function fixture() { } #[Test] public function overriding_non_existant_method() { Assert::equals( - '%T::nonExistant() has #[\Override] attribute, but no matching parent method exists', + 'T::nonExistant() has #[\Override] attribute, but no matching parent method exists', $this->verify('class %T extends \lang\Throwable { #[Override] public function nonExistant() { } From c4a13765ec3b37b09c55b6c083dc63a2dbad8dcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 18:28:43 +0200 Subject: [PATCH 769/926] Always check `#[Override]` during compilation --- .../php/lang/ast/emit/CheckOverride.class.php | 38 ------------------- src/main/php/lang/ast/emit/PHP.class.php | 34 +++++++++++++++-- src/main/php/lang/ast/emit/PHP70.class.php | 1 - src/main/php/lang/ast/emit/PHP71.class.php | 1 - src/main/php/lang/ast/emit/PHP72.class.php | 1 - src/main/php/lang/ast/emit/PHP73.class.php | 1 - src/main/php/lang/ast/emit/PHP74.class.php | 1 - src/main/php/lang/ast/emit/PHP80.class.php | 1 - src/main/php/lang/ast/emit/PHP81.class.php | 1 - src/main/php/lang/ast/emit/PHP82.class.php | 1 - .../emit/MethodOverridingTest.class.php | 13 ------- 11 files changed, 31 insertions(+), 62 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/CheckOverride.class.php diff --git a/src/main/php/lang/ast/emit/CheckOverride.class.php b/src/main/php/lang/ast/emit/CheckOverride.class.php deleted file mode 100755 index 9ed8abdc..00000000 --- a/src/main/php/lang/ast/emit/CheckOverride.class.php +++ /dev/null @@ -1,38 +0,0 @@ -annotations && $method->annotations->named(Override::class)) { - - // Check parent class - if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod($method->name)) goto emit; - - // Check all implemented interfaces - foreach ($result->codegen->lookup('self')->implementedInterfaces() as $interface) { - if ($result->codegen->lookup($interface)->providesMethod($method->name)) goto emit; - } - - throw new Error( - sprintf( - '%s::%s() has #[\\Override] attribute, but no matching parent method exists', - substr($result->codegen->scope[0]->type->name, 1), - $method->name - ), - $result->codegen->source, - $method->line - ); - } - - emit: parent::emitMethod($result, $method); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1f6ffb34..a947832c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1,5 +1,6 @@ comment && $this->emitOne($result, $method->comment); - $method->annotations && $this->emitOne($result, $method->annotations); - $result->at($method->declared)->out->write( + + if ($method->annotations) { + $this->emitOne($result, $method->annotations); + + // Verify `Override` if existant. Although PHP 8.3+ includes this compile-time + // check, it does not come with a measurable performance impact doing so here, + // and we prevent uncatchable errors this way. + if (!$method->annotations->named(Override::class)) goto declaration; + + // Check parent class + if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod($method->name)) goto declaration; + + // Check all implemented interfaces + foreach ($result->codegen->lookup('self')->implementedInterfaces() as $interface) { + if ($result->codegen->lookup($interface)->providesMethod($method->name)) goto declaration; + } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + substr($result->codegen->scope[0]->type->name, 1), + $method->name + ), + $result->codegen->source, + $method->line + ); + } + + declaration: $result->at($method->declared)->out->write( implode(' ', $method->modifiers). ' function '. ($method->signature->byref ? '&' : ''). diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 3a18ed07..49b90231 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -14,7 +14,6 @@ class PHP70 extends PHP { ArbitrayNewExpressions, ArrayUnpackUsingMerge, AttributesAsComments, - CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index b99a76da..ecf1ea1c 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -13,7 +13,6 @@ class PHP71 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, - CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 36756e5e..21ef7b23 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -13,7 +13,6 @@ class PHP72 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, - CheckOverride, ForeachDestructuringAsStatement, MatchAsTernaries, NonCapturingCatchVariables, diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index 123a569f..8b0c0fd9 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -15,7 +15,6 @@ class PHP73 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, - CheckOverride, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index c6087fc3..77fe94a1 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -13,7 +13,6 @@ class PHP74 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, - CheckOverride, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 34ea170c..48f37fd0 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -21,7 +21,6 @@ class PHP80 extends PHP { use ArrayUnpackUsingMerge, CallablesAsClosures, - CheckOverride, OmitConstantTypes, ReadonlyClasses, ReadonlyProperties, diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index cdc07916..ce52501f 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -20,7 +20,6 @@ */ class PHP81 extends PHP { use - CheckOverride, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 708eea1b..5e508762 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -20,7 +20,6 @@ */ class PHP82 extends PHP { use - CheckOverride, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index cc66790f..38985e5a 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -1,6 +1,5 @@ = 8.3: PHP will raise errors for us - if (PHP_VERSION_ID >= 80300) { - $rt= Runtime::getInstance()->newInstance(null, 'class', 'xp.runtime.Evaluate', [str_replace('%T', 'T', $code)]); - $rt->in->close(); - $err= $rt->err->readLine(); - $rt->close(); - - return false === $err ? null : preg_replace('/Uncaught error: Compile error \((.+)\)/', '$1', $err); - } - - // PHP < 8.3: XP compiler will raise errors for us try { $t= $this->declare($code); return null; From d5aba467c3e41e395cdddfd94fe9d75b62ae3c5e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 19:10:09 +0200 Subject: [PATCH 770/926] Do not verify overriding inside traits This needs to be done at the time they are *used* inside a type --- src/main/php/lang/ast/emit/PHP.class.php | 1 + .../lang/ast/unittest/emit/MethodOverridingTest.class.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index a947832c..f8e645d5 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -637,6 +637,7 @@ protected function emitMethod($result, $method) { // check, it does not come with a measurable performance impact doing so here, // and we prevent uncatchable errors this way. if (!$method->annotations->named(Override::class)) goto declaration; + if ($result->codegen->scope[0]->type->is('trait')) goto declaration; // Check parent class if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod($method->name)) goto declaration; diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index 38985e5a..43d68130 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -69,4 +69,12 @@ public function nonExistant() { } }' )); } + + #[Test] + public function override_inside_traits() { + Assert::null($this->verify('trait %T { + #[Override] + public function run() { } + }')); + } } \ No newline at end of file From 9de4f06d2b787a3d7a552d77e5829b14834609d3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 19:16:10 +0200 Subject: [PATCH 771/926] Fix overwriting parent methods inside interfaces --- src/main/php/lang/ast/CodeGen.class.php | 2 +- src/main/php/lang/ast/emit/Declaration.class.php | 12 +++++++++--- .../ast/unittest/emit/MethodOverridingTest.class.php | 8 ++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index b0e82cce..a8ed6635 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -48,7 +48,7 @@ public function lookup($type) { if ('self' === $type || 'static' === $type) { return new Declaration($enclosing->type); } else if ('parent' === $type) { - return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null; + return isset($enclosing->type->parent) ? $this->lookup($enclosing->type->parent->literal()) : null; } foreach ($this->scope as $scope) { diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 44c617bc..87a76ef2 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,6 +1,6 @@ type->name, '\\'); } /** @return iterable */ public function implementedInterfaces() { - foreach ($this->type->implements as $interface) { - yield $interface->literal(); + if ($this->type instanceof InterfaceDeclaration) { + foreach ($this->type->parents as $interface) { + yield $interface->literal(); + } + } else { + foreach ($this->type->implements as $interface) { + yield $interface->literal(); + } } } diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index 43d68130..6b435e0f 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -48,6 +48,14 @@ public function run() { } }')); } + #[Test] + public function correctly_overwriting_interface_method() { + Assert::null($this->verify('interface %T extends \lang\Runnable { + #[Override] + public function run(); + }')); + } + #[Test] public function without_parent() { Assert::equals( From 25b64df5da33ce148bca66d4a69798f4248afee4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 19:29:22 +0200 Subject: [PATCH 772/926] Verify override checks inside anonymous classes --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- .../ast/unittest/emit/MethodOverridingTest.class.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f8e645d5..1c2bfb3a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -650,7 +650,7 @@ protected function emitMethod($result, $method) { throw new Error( sprintf( '%s::%s() has #[\\Override] attribute, but no matching parent method exists', - substr($result->codegen->scope[0]->type->name, 1), + substr($result->codegen->scope[0]->type->name ?? '$class@anonymous', 1), $method->name ), $result->codegen->source, diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index 6b435e0f..0d48e106 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -67,6 +67,17 @@ public function fixture() { } )); } + #[Test] + public function without_parent_in_anonymous_class() { + Assert::equals( + 'class@anonymous::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('new class() { + #[Override] + public function fixture() { } + };' + )); + } + #[Test] public function overriding_non_existant_method() { Assert::equals( From 69454e91bdc8222c514ab2c24cfb09420b91f9c2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 20:00:47 +0200 Subject: [PATCH 773/926] Verify trait methods using `#[Override]` --- .../php/lang/ast/emit/Declaration.class.php | 16 ++++- .../php/lang/ast/emit/Incomplete.class.php | 10 +++ src/main/php/lang/ast/emit/PHP.class.php | 69 ++++++++++++------- .../php/lang/ast/emit/Reflection.class.php | 13 ++++ src/main/php/lang/ast/emit/Type.class.php | 8 +++ .../emit/MethodOverridingTest.class.php | 13 ++++ 6 files changed, 104 insertions(+), 25 deletions(-) diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 87a76ef2..9f16e92d 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,6 +1,6 @@ body["{$named}()"]); } + /** + * Returns all methods annotated with a given annotation + * + * @param string $annotation + * @return iterable + */ + public function methodsAnnotated($annotation) { + foreach ($this->body as $member) { + if ($member instanceof Method && $member->annotations && $member->annotations->named($annotation)) { + yield $member->name => $member->line; + } + } + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php index fb4d164f..70181ae8 100755 --- a/src/main/php/lang/ast/emit/Incomplete.class.php +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -22,6 +22,16 @@ public function providesMethod($named) { return false; } + /** + * Returns all methods annotated with a given annotation + * + * @param string $annotation + * @return iterable + */ + public function methodsAnnotated($annotation) { + return []; + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1c2bfb3a..110d3346 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -154,6 +154,39 @@ protected function enclose($result, $node, $signature, $static, $emit) { $result->locals= array_pop($result->stack); } + /** + * Verify `Override` if existant. Although PHP 8.3+ includes this compile-time + * check, it does not come with a measurable performance impact doing so here, + * and we prevent uncatchable errors this way. + * + * @param lang.ast.CodeGen $codegen + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + protected function checkOverride($codegen, $method, $line) { + if ($codegen->scope[0]->type->is('trait')) return; + + // Check parent class + if (($parent= $codegen->lookup('parent')) && $parent->providesMethod($method)) return; + + // Check all implemented interfaces + foreach ($codegen->lookup('self')->implementedInterfaces() as $interface) { + if ($codegen->lookup($interface)->providesMethod($method)) return; + } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + substr($codegen->scope[0]->type->name ?? '$class@anonymous', 1), + $method + ), + $codegen->source, + $line + ); + } + /** * Emits local initializations * @@ -565,6 +598,14 @@ protected function emitTrait($result, $trait) { protected function emitUse($result, $use) { $result->out->write('use '.implode(',', $use->types)); + + // Verify Override + foreach ($use->types as $type) { + foreach ($result->codegen->lookup($type)->methodsAnnotated(Override::class) as $method => $line) { + $this->checkOverride($result->codegen, $method, $line); + } + } + if ($use->aliases) { $result->out->write('{'); foreach ($use->aliases as $reference => $alias) { @@ -629,36 +670,16 @@ protected function emitMethod($result, $method) { ]; $method->comment && $this->emitOne($result, $method->comment); - if ($method->annotations) { $this->emitOne($result, $method->annotations); - - // Verify `Override` if existant. Although PHP 8.3+ includes this compile-time - // check, it does not come with a measurable performance impact doing so here, - // and we prevent uncatchable errors this way. - if (!$method->annotations->named(Override::class)) goto declaration; - if ($result->codegen->scope[0]->type->is('trait')) goto declaration; - - // Check parent class - if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod($method->name)) goto declaration; - - // Check all implemented interfaces - foreach ($result->codegen->lookup('self')->implementedInterfaces() as $interface) { - if ($result->codegen->lookup($interface)->providesMethod($method->name)) goto declaration; - } - - throw new Error( - sprintf( - '%s::%s() has #[\\Override] attribute, but no matching parent method exists', - substr($result->codegen->scope[0]->type->name ?? '$class@anonymous', 1), - $method->name - ), - $result->codegen->source, + $method->annotations->named(Override::class) && $this->checkOverride( + $result->codegen, + $method->name, $method->line ); } - declaration: $result->at($method->declared)->out->write( + $result->at($method->declared)->out->write( implode(' ', $method->modifiers). ' function '. ($method->signature->byref ? '&' : ''). diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index d1bc46c8..7d50c2de 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -1,5 +1,6 @@ reflect->hasMethod($named); } + /** + * Returns all methods annotated with a given annotation + * + * @param string $annotation + * @return iterable + */ + public function methodsAnnotated($annotation) { + foreach ((new Reflect($this->reflect))->methods()->annotated($annotation) as $method) { + yield $method->name() => $this->reflect->getMethod($method->name())->getStartLine(); + } + } + /** * Returns whether a given member is an enum case * diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 1c5ff645..9d0a43ed 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -22,6 +22,14 @@ public abstract function implementedInterfaces(); */ public abstract function providesMethod($named); + /** + * Returns all methods annotated with a given annotation + * + * @param string $annotation + * @return iterable + */ + public abstract function methodsAnnotated($annotation); + /** * Returns whether a given member is an enum case * diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index 0d48e106..d33ed39d 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -96,4 +96,17 @@ public function override_inside_traits() { public function run() { } }')); } + + #[Test] + public function without_parent_with_method_from_trait() { + $trait= $this->declare('trait %T { + #[Override] + public function run() { } + }'); + + Assert::equals( + 'T::run() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T { use '.$trait->literal().'; }') + ); + } } \ No newline at end of file From da7a5a81151d48b9d27860d5cbfe68f1141ff9fd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 21:05:55 +0200 Subject: [PATCH 774/926] QA: Remove superfluous assignment --- .../php/lang/ast/unittest/emit/MethodOverridingTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index d33ed39d..749a2d01 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -20,7 +20,7 @@ protected function emitters() { return []; } */ private function verify($code) { try { - $t= $this->declare($code); + $this->declare($code); return null; } catch (Error $e) { return preg_replace('/T[0-9]+/', 'T', $e->getMessage()); From dc943273331a35b6280cb87f53980c5561909908 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 21:12:23 +0200 Subject: [PATCH 775/926] QA: Simplify implementation of methodsAnnotated() --- src/main/php/lang/ast/emit/Reflection.class.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 7d50c2de..eaf128ef 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -1,6 +1,6 @@ reflect))->methods()->annotated($annotation) as $method) { - yield $method->name() => $this->reflect->getMethod($method->name())->getStartLine(); + $meta= Reflect::meta(); + foreach ($this->reflect->getMethods() as $method) { + if (isset($meta->methodAnnotations($method)[$annotation])) { + yield $method->getName() => $method->getStartLine(); + } } } From 507e6c274c0367a040954db62afde78fe1ebb0ff Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 21:12:51 +0200 Subject: [PATCH 776/926] Mark xp-framework/reflection as runtime dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 56dcd208..feee80ed 100755 --- a/composer.json +++ b/composer.json @@ -7,11 +7,11 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^11.6 | ^10.16", + "xp-framework/reflection": "^2.13", "xp-framework/ast": "^10.1", "php" : ">=7.0.0" }, "require-dev" : { - "xp-framework/reflection": "^2.13", "xp-framework/test": "^1.5" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], From 5bfaa52d56827974a2dbc1f2116b63b737eef9ed Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:02:09 +0200 Subject: [PATCH 777/926] Move checkOverride[s]() to Type implementations See https://github.com/xp-framework/compiler/pull/173#discussion_r1264537749 --- src/main/php/lang/ast/CodeGen.class.php | 4 +- .../php/lang/ast/emit/Declaration.class.php | 75 +++++++++++++------ .../php/lang/ast/emit/Incomplete.class.php | 26 +++++-- src/main/php/lang/ast/emit/PHP.class.php | 40 +--------- .../php/lang/ast/emit/Reflection.class.php | 49 +++++++++--- src/main/php/lang/ast/emit/Type.class.php | 22 ++++-- .../unittest/emit/GeneratedCodeTest.class.php | 4 +- 7 files changed, 131 insertions(+), 89 deletions(-) diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index a8ed6635..c17c5add 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -46,14 +46,14 @@ public function lookup($type) { $enclosing= $this->scope[0] ?? null; if ('self' === $type || 'static' === $type) { - return new Declaration($enclosing->type); + return new Declaration($enclosing->type, $this); } else if ('parent' === $type) { return isset($enclosing->type->parent) ? $this->lookup($enclosing->type->parent->literal()) : null; } foreach ($this->scope as $scope) { if ($scope->type->name && $type === $scope->type->name->literal()) { - return new Declaration($scope->type); + return new Declaration($scope->type, $this); } } diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 9f16e92d..1510f2b0 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,31 +1,74 @@ type= $type; + $this->codegen= $codegen; } /** @return string */ public function name() { return ltrim($this->type->name, '\\'); } - /** @return iterable */ - public function implementedInterfaces() { - if ($this->type instanceof InterfaceDeclaration) { + /** + * Checks `#[Override]` + * + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error + */ + public function checkOverrides($type) { + foreach ($this->type->body as $member) { + if ($member instanceof Method && $member->annotations && $member->annotations->named($annotation)) { + $type->checkOverride($member->name, $member->line); + } + } + } + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public function checkOverride($method, $line) { + if ($this->type instanceof TraitDeclaration) { + + // Do not check traits, this is done when including them into the type + return; + } else if ($this->type instanceof InterfaceDeclaration) { + + // Check parent interfaces foreach ($this->type->parents as $interface) { - yield $interface->literal(); + if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; } } else { + + // Check parent, then check all implemented interfaces + if ($this->type->parent && $this->codegen->lookup('parent')->providesMethod($method)) return; foreach ($this->type->implements as $interface) { - yield $interface->literal(); + if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; } } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + isset($this->type->name) ? substr($this->type->name->literal(), 1) : 'class@anonymous', + $method + ), + $this->codegen->source, + $line + ); } /** @@ -35,21 +78,7 @@ public function implementedInterfaces() { * @return bool */ public function providesMethod($named) { - return isset($this->body["{$named}()"]); - } - - /** - * Returns all methods annotated with a given annotation - * - * @param string $annotation - * @return iterable - */ - public function methodsAnnotated($annotation) { - foreach ($this->body as $member) { - if ($member instanceof Method && $member->annotations && $member->annotations->named($annotation)) { - yield $member->name => $member->line; - } - } + return isset($this->type->body["{$named}()"]); } /** diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php index 70181ae8..8ba0936b 100755 --- a/src/main/php/lang/ast/emit/Incomplete.class.php +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -9,9 +9,6 @@ public function __construct($name) { $this->name= $name; } /** @return string */ public function name() { return $this->name; } - /** @return iterable */ - public function implementedInterfaces() { return []; } - /** * Checks whether a given method exists * @@ -23,13 +20,26 @@ public function providesMethod($named) { } /** - * Returns all methods annotated with a given annotation + * Checks `#[Override]` + * + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error + */ + public function checkOverrides($type) { + // NOOP + } + + /** + * Checks `#[Override]` for a given method * - * @param string $annotation - * @return iterable + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error */ - public function methodsAnnotated($annotation) { - return []; + public function checkOverride($method, $line) { + // NOOP } /** diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 110d3346..805f5326 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -154,39 +154,6 @@ protected function enclose($result, $node, $signature, $static, $emit) { $result->locals= array_pop($result->stack); } - /** - * Verify `Override` if existant. Although PHP 8.3+ includes this compile-time - * check, it does not come with a measurable performance impact doing so here, - * and we prevent uncatchable errors this way. - * - * @param lang.ast.CodeGen $codegen - * @param string $method - * @param int $line - * @return void - * @throws lang.ast.Error - */ - protected function checkOverride($codegen, $method, $line) { - if ($codegen->scope[0]->type->is('trait')) return; - - // Check parent class - if (($parent= $codegen->lookup('parent')) && $parent->providesMethod($method)) return; - - // Check all implemented interfaces - foreach ($codegen->lookup('self')->implementedInterfaces() as $interface) { - if ($codegen->lookup($interface)->providesMethod($method)) return; - } - - throw new Error( - sprintf( - '%s::%s() has #[\\Override] attribute, but no matching parent method exists', - substr($codegen->scope[0]->type->name ?? '$class@anonymous', 1), - $method - ), - $codegen->source, - $line - ); - } - /** * Emits local initializations * @@ -601,9 +568,7 @@ protected function emitUse($result, $use) { // Verify Override foreach ($use->types as $type) { - foreach ($result->codegen->lookup($type)->methodsAnnotated(Override::class) as $method => $line) { - $this->checkOverride($result->codegen, $method, $line); - } + $result->codegen->lookup($type)->checkOverrides($result->codegen->lookup('self')); } if ($use->aliases) { @@ -672,8 +637,7 @@ protected function emitMethod($result, $method) { $method->comment && $this->emitOne($result, $method->comment); if ($method->annotations) { $this->emitOne($result, $method->annotations); - $method->annotations->named(Override::class) && $this->checkOverride( - $result->codegen, + $method->annotations->named(Override::class) && $result->codegen->lookup('self')->checkOverride( $method->name, $method->line ); diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index eaf128ef..e000d2d3 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -1,5 +1,6 @@ reflect->name; } - /** @return iterable */ - public function implementedInterfaces() { return $this->type->getInterfaceNames(); } - /** * Checks whether a given method exists * @@ -38,18 +36,51 @@ public function providesMethod($named) { } /** - * Returns all methods annotated with a given annotation + * Checks `#[Override]` * - * @param string $annotation - * @return iterable + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error */ - public function methodsAnnotated($annotation) { + public function checkOverrides($type) { $meta= Reflect::meta(); foreach ($this->reflect->getMethods() as $method) { - if (isset($meta->methodAnnotations($method)[$annotation])) { - yield $method->getName() => $method->getStartLine(); + if (isset($meta->methodAnnotations($method)[Override::class])) { + $type->checkOverride($method->getName(), $method->getStartLine()); + } + } + } + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public function checkOverride($method, $line) { + + // Ignore traits, check parents and interfaces for all other types + if ($this->reflect->isTrait()) { + return; + } else if ($parent= $this->reflect->getParentClass()) { + if ($parent->hasMethod($method)) return; + } else { + foreach ($this->type->getInterfaces() as $interface) { + if ($interface->hasMethod($method)) return; } } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + $this->reflect->isAnonymous() ? 'class@anonymous' : $this->reflect->getName(), + $method + ), + $this->reflect->getFileName(), + $line + ); } /** diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 9d0a43ed..95ab576a 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -11,9 +11,6 @@ static function __static() { /** @return string */ public abstract function name(); - /** @return iterable */ - public abstract function implementedInterfaces(); - /** * Checks whether a given method exists * @@ -23,12 +20,23 @@ public abstract function implementedInterfaces(); public abstract function providesMethod($named); /** - * Returns all methods annotated with a given annotation + * Checks `#[Override]` + * + * @param self $type + * @return void + * @throws lang.ast.Error + */ + public abstract function checkOverrides($type); + + /** + * Checks `#[Override]` for a given method * - * @param string $annotation - * @return iterable + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error */ - public abstract function methodsAnnotated($annotation); + public abstract function checkOverride($method, $line); /** * Returns whether a given member is an enum case diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index a5fc9402..2cbb7eba 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -49,7 +49,7 @@ public function lookup_self() { $r= new GeneratedCode(new MemoryOutputStream()); $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($context->type), $r->lookup('self')); + Assert::equals(new Declaration($context->type, $r->codegen), $r->lookup('self')); } #[Test] @@ -73,7 +73,7 @@ public function lookup_named() { $r= new GeneratedCode(new MemoryOutputStream()); $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - Assert::equals(new Declaration($context->type), $r->lookup('\\T')); + Assert::equals(new Declaration($context->type, $r->codegen), $r->lookup('\\T')); } #[Test] From 489c773f84f3d90577590e2b015d48cba9335e98 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:05:18 +0200 Subject: [PATCH 778/926] QA: Use imports --- src/main/php/lang/ast/emit/Reflection.class.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index e000d2d3..97f7a727 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -1,7 +1,8 @@ reflect= new \ReflectionClass($type); - } catch (\ReflectionException $e) { + $this->reflect= new ReflectionClass($type); + } catch (ReflectionException $e) { throw new ClassNotFoundException($type); } } @@ -92,9 +93,9 @@ public function checkOverride($method, $line) { public function rewriteEnumCase($member) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); - return $value instanceof \UnitEnum; + return $value instanceof UnitEnum; } return false; } From 5cc6cd2b90f09896fdb7dc2e02e15357b3ce9170 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:05:28 +0200 Subject: [PATCH 779/926] Fix Declaration::checkOverrides() --- src/main/php/lang/ast/emit/Declaration.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 1510f2b0..66103718 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,5 +1,6 @@ type->name, '\\'); } */ public function checkOverrides($type) { foreach ($this->type->body as $member) { - if ($member instanceof Method && $member->annotations && $member->annotations->named($annotation)) { + if ($member instanceof Method && $member->annotations && $member->annotations->named(Override::class)) { $type->checkOverride($member->name, $member->line); } } From 7974c4a127b0819a0c8c7cda0cdb21e870797db6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:07:04 +0200 Subject: [PATCH 780/926] QA: WS --- src/main/php/lang/ast/Emitter.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 683e04d0..7e958c96 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -172,7 +172,6 @@ protected abstract function result($target); /** * Emitter entry point, takes nodes and emits them to the given target. - * * * @param iterable $nodes * @param io.streams.OutputStream $target From 7b155784061c2bc3be4a0a2b69d7a9d5dec9e121 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:21:59 +0200 Subject: [PATCH 781/926] Make private method handling consisten with RFC --- src/main/php/lang/ast/emit/Declaration.class.php | 11 ++++++++--- src/main/php/lang/ast/emit/Incomplete.class.php | 3 ++- src/main/php/lang/ast/emit/Reflection.class.php | 8 ++++++-- src/main/php/lang/ast/emit/Type.class.php | 3 ++- .../unittest/emit/MethodOverridingTest.class.php | 13 +++++++++++++ 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 66103718..802b7720 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -3,6 +3,7 @@ use Override; use lang\ast\Error; use lang\ast\nodes\{EnumCase, InterfaceDeclaration, TraitDeclaration, Property, Method}; +use lang\reflection\Modifiers; class Declaration extends Type { private $type, $codegen; @@ -55,7 +56,7 @@ public function checkOverride($method, $line) { } else { // Check parent, then check all implemented interfaces - if ($this->type->parent && $this->codegen->lookup('parent')->providesMethod($method)) return; + if ($this->type->parent && $this->codegen->lookup('parent')->providesMethod($method, MODIFIER_PUBLIC)) return; foreach ($this->type->implements as $interface) { if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; } @@ -76,10 +77,14 @@ public function checkOverride($method, $line) { * Checks whether a given method exists * * @param string $named + * @param ?int $select * @return bool */ - public function providesMethod($named) { - return isset($this->type->body["{$named}()"]); + public function providesMethod($named, $select= null) { + if ($method= $this->type->body["{$named}()"] ?? null) { + return null === $select || (bool)((new Modifiers($method->modifiers))->bits() & $select); + } + return false; } /** diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php index 8ba0936b..dfda0688 100755 --- a/src/main/php/lang/ast/emit/Incomplete.class.php +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -13,9 +13,10 @@ public function name() { return $this->name; } * Checks whether a given method exists * * @param string $named + * @param ?int $select * @return bool */ - public function providesMethod($named) { + public function providesMethod($named, $select= null) { return false; } diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 97f7a727..bd19d968 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -30,10 +30,14 @@ public function name() { return $this->reflect->name; } * Checks whether a given method exists * * @param string $named + * @param ?int $select * @return bool */ - public function providesMethod($named) { - return $this->reflect->hasMethod($named); + public function providesMethod($named, $select= null) { + if ($this->reflect->hasMethod($named)) { + return null === $select || (bool)($this->reflect->getMethod($named)->getModifiers() & $select); + } + return false; } /** diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 95ab576a..642f9008 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -15,9 +15,10 @@ public abstract function name(); * Checks whether a given method exists * * @param string $named + * @param ?int $select * @return bool */ - public abstract function providesMethod($named); + public abstract function providesMethod($named, $select= null); /** * Checks `#[Override]` diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php index 749a2d01..a7cc07e5 100755 --- a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -109,4 +109,17 @@ public function run() { } $this->verify('class %T { use '.$trait->literal().'; }') ); } + + #[Test] + public function private_methods_not_considered() { + $parent= $this->declare('class %T { private function fixture() { } }'); + + Assert::equals( + 'T::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T extends '.$parent->literal().' { + #[Override] + public function fixture() { } + }') + ); + } } \ No newline at end of file From f4f95b30007c766102fb57fdc4a27706ac319a1d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jul 2023 23:24:07 +0200 Subject: [PATCH 782/926] Only take non-private parent methods into account --- src/main/php/lang/ast/emit/Declaration.class.php | 9 +++++++-- src/main/php/lang/ast/emit/Reflection.class.php | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 802b7720..3a23f906 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -55,8 +55,13 @@ public function checkOverride($method, $line) { } } else { - // Check parent, then check all implemented interfaces - if ($this->type->parent && $this->codegen->lookup('parent')->providesMethod($method, MODIFIER_PUBLIC)) return; + // Check parent for non-private methods + if ($this->type->parent && $this->codegen->lookup($this->type->parent->literal())->providesMethod( + $method, + MODIFIER_PUBLIC | MODIFIER_PROTECTED + )) return; + + // Finally, check all implemented interfaces foreach ($this->type->implements as $interface) { if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; } diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index bd19d968..383f7640 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -70,7 +70,9 @@ public function checkOverride($method, $line) { if ($this->reflect->isTrait()) { return; } else if ($parent= $this->reflect->getParentClass()) { - if ($parent->hasMethod($method)) return; + if ($parent->hasMethod($method)) { + if (!$this->reflect->getMethod($named)->isPrivate()) return; + } } else { foreach ($this->type->getInterfaces() as $interface) { if ($interface->hasMethod($method)) return; From ef1f9523d41041318cf4b7fc5fcf8e1158b849f3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 14:26:20 +0200 Subject: [PATCH 783/926] QA: Remove unused import --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 805f5326..4b8f7d36 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -17,7 +17,7 @@ Variable }; use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsExpression}; -use lang\ast\{Emitter, Error, Node, Type, Result}; +use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { const PROPERTY = 0; From c31ecdb0efd33d7224550f76c483406d899a65c8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 14:27:05 +0200 Subject: [PATCH 784/926] Extract `self` lookup from loop --- src/main/php/lang/ast/emit/PHP.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 4b8f7d36..57bb5cfc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -567,8 +567,9 @@ protected function emitUse($result, $use) { $result->out->write('use '.implode(',', $use->types)); // Verify Override + $self= $result->codegen->lookup('self'); foreach ($use->types as $type) { - $result->codegen->lookup($type)->checkOverrides($result->codegen->lookup('self')); + $result->codegen->lookup($type)->checkOverrides($self); } if ($use->aliases) { From 1b891c80d9f95cc5409820b7afa5ee25a256ffe5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 14:29:35 +0200 Subject: [PATCH 785/926] Remove superfluous bool casts The result of an `||` operation is always a bool --- src/main/php/lang/ast/emit/Declaration.class.php | 2 +- src/main/php/lang/ast/emit/Reflection.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php index 3a23f906..190038f2 100755 --- a/src/main/php/lang/ast/emit/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -87,7 +87,7 @@ public function checkOverride($method, $line) { */ public function providesMethod($named, $select= null) { if ($method= $this->type->body["{$named}()"] ?? null) { - return null === $select || (bool)((new Modifiers($method->modifiers))->bits() & $select); + return null === $select || (new Modifiers($method->modifiers))->bits() & $select; } return false; } diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 383f7640..b7f26660 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -35,7 +35,7 @@ public function name() { return $this->reflect->name; } */ public function providesMethod($named, $select= null) { if ($this->reflect->hasMethod($named)) { - return null === $select || (bool)($this->reflect->getMethod($named)->getModifiers() & $select); + return null === $select || $this->reflect->getMethod($named)->getModifiers() & $select; } return false; } From 09826415002d15afa39445de38d5287c2680ce9a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 14:47:56 +0200 Subject: [PATCH 786/926] Release 8.15.0 --- ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 78636ead..3dd01a62 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.15.0 / 2023-07-16 + +* Merged PR #173: Check for `#[Override]` annotation, implementing this + PHP 8.3 RFC: https://wiki.php.net/rfc/marking_overriden_methods + (@thekid) * Merged PR #171: Refactor scope lookup from Result to CodeGen - @thekid ## 8.14.0 / 2023-07-15 From 78d20f4b953d5dee2b10e1b9407d4053322511c8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 15:15:32 +0200 Subject: [PATCH 787/926] Fix parent constructors not being invoked w/ non-constant properties --- ChangeLog.md | 4 ++++ src/main/php/lang/ast/emit/PHP.class.php | 9 ++++++++- .../InitializeWithExpressionsTest.class.php | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 3dd01a62..1f9da332 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed parent constructors not being invoked when using non-constant + property initialization. + (@thekid) + ## 8.15.0 / 2023-07-16 * Merged PR #173: Check for `#[Override]` annotation, implementing this diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 57bb5cfc..d1af26ce 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -469,7 +469,14 @@ protected function emitClass($result, $class) { // Create constructor for property initializations to non-static scalars // which have not already been emitted inside constructor if ($context->init) { - $result->out->write('public function __construct() {'); + $t= $result->temp(); + $result->out->write("public function __construct(... {$t}) {"); + + // If existant, invoke parent constructor, passing all parameters as arguments + if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod('__construct')) { + $result->out->write("parent::__construct(... {$t});"); + } + $this->emitInitializations($result, $context->init); $result->out->write('}'); } diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php index 940bca5c..8b5b674b 100755 --- a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -216,4 +216,24 @@ public function run() { }'); Assert::equals(1, $t->newInstance(new Handle(1))->run()); } + + #[Test] + public function invokes_parent_constructor() { + $t= $this->declare('class %T { + protected $invoked= false; + + public function __construct($invoked) { + $this->invoked= $invoked; + } + }'); + + $r= $this->declare('use lang\ast\unittest\emit\Handle; class %T extends '.$t->literal().' { + private $h= new Handle(0); + + public function run() { + return [$this->invoked, $this->h]; + } + }'); + Assert::equals([true, new Handle(0)], $r->newInstance(true)->run()); + } } \ No newline at end of file From fa16132534931f7fe9ec37756f72cb23cf302706 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 16 Jul 2023 15:15:49 +0200 Subject: [PATCH 788/926] Release 8.15.1 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 1f9da332..d8deb3a0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.15.1 / 2023-07-16 + * Fixed parent constructors not being invoked when using non-constant property initialization. (@thekid) From 77c0f24795c33add9ed71e9f515540062321418b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 11:15:55 +0200 Subject: [PATCH 789/926] Upgrade to 8.6.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a5dff3..05af35d0 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.6.1.sh > xp-run && + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.6.2.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From 3c1b79538e5db258cbbfb5b190a0cc145e89dc76 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 11:19:31 +0200 Subject: [PATCH 790/926] Refactor code to work without lang.reflect.Package See https://github.com/xp-framework/rfc/issues/338 --- .../lang/ast/CompilingClassloader.class.php | 1 - src/main/php/lang/ast/Emitter.class.php | 13 +++++------ src/main/php/xp/compiler/Usage.class.php | 22 ++++++++++++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 5b70fbd8..02031d10 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -2,7 +2,6 @@ use lang\ast\emit\Reflection; use lang\ast\emit\php\XpMeta; -use lang\reflect\Package; use lang\{ ClassFormatException, ClassLoader, diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 7e958c96..78cb79d5 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -2,7 +2,6 @@ use io\streams\OutputStream; use lang\ast\{Node, Error, Errors}; -use lang\reflect\Package; use lang\{IllegalArgumentException, IllegalStateException, ClassLoader, XPClass}; abstract class Emitter { @@ -18,16 +17,16 @@ abstract class Emitter { */ public static function forRuntime($runtime, $emitters= []) { sscanf($runtime, '%[^.:]%*[.:]%d.%d', $engine, $major, $minor); - $p= Package::forName('lang.ast.emit'); + $cl= ClassLoader::getDefault(); $engine= strtoupper($engine); do { - $impl= $engine.$major.$minor; - if ($p->providesClass($impl)) { - if (empty($emitters)) return $p->loadClass($impl); + $impl= "lang.ast.emit.{$engine}{$major}{$minor}"; + if ($cl->providesClass($impl)) { + if (empty($emitters)) return $cl->loadClass($impl); // Extend loaded class, including all given emitters - $extended= ['kind' => 'class', 'extends' => [$p->loadClass($impl)], 'implements' => [], 'use' => []]; + $extended= ['kind' => 'class', 'extends' => [$cl->loadClass($impl)], 'implements' => [], 'use' => []]; foreach ($emitters as $class) { if ($class instanceof XPClass) { $impl.= '⋈'.strtr($class->getName(), ['.' => '·']); @@ -37,7 +36,7 @@ public static function forRuntime($runtime, $emitters= []) { $extended['use'][]= XPClass::forName($class); } } - return ClassLoader::defineType($p->getName().'.'.$impl, $extended, '{}'); + return ClassLoader::defineType($impl, $extended, '{}'); } } while ($minor-- > 0); diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index b2208f14..3869db2f 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,13 +1,29 @@ packageContents($package) as $item) { + if (0 === substr_compare($item, \xp::CLASS_FILE_EXT, $offset)) { + yield $cl->loadClass($package.'.'.substr($item, 0, $offset)); + } + } + } + /** @return int */ public static function main(array $args) { Console::$err->writeLine('Usage: xp compile []'); @@ -21,14 +37,14 @@ public function add($t, $active= false) { }; $emitter= Emitter::forRuntime(self::RUNTIME.':'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION); - foreach (Package::forName('lang.ast.emit')->getClasses() as $class) { + foreach (self::classesIn('lang.ast.emit') as $class) { if ($class->isSubclassOf(Emitter::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { $impl->add($class, $class->equals($emitter)); } } $language= Language::named(strtoupper(self::RUNTIME)); - foreach (Package::forName('lang.ast.syntax')->getClasses() as $class) { + foreach (self::classesIn('lang.ast.syntax') as $class) { if ($class->isSubclassOf(Language::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { $impl->add($class, $class->isInstance($language)); } From 896eb4f138112e7ce8c8b280d3aee7ab5008a808 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 11:30:21 +0200 Subject: [PATCH 791/926] Only get default class loader once --- src/main/php/xp/compiler/Usage.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 3869db2f..0725e7af 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -6,7 +6,7 @@ /** @codeCoverageIgnore */ class Usage { - const RUNTIME = 'php'; + const RUNTIME= 'php'; /** * Returns XPClass instances for all classes inside a given package @@ -17,7 +17,7 @@ class Usage { private static function classesIn($package) { $offset= -strlen(\xp::CLASS_FILE_EXT); $cl= ClassLoader::getDefault(); - foreach (ClassLoader::getDefault()->packageContents($package) as $item) { + foreach ($cl->packageContents($package) as $item) { if (0 === substr_compare($item, \xp::CLASS_FILE_EXT, $offset)) { yield $cl->loadClass($package.'.'.substr($item, 0, $offset)); } From 775fe23e09579f435109c5499827920d2b90ca40 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 11:46:50 +0200 Subject: [PATCH 792/926] Rewrite to use new reflection API --- src/main/php/xp/compiler/Usage.class.php | 35 +++++++----------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php index 0725e7af..fdcef972 100755 --- a/src/main/php/xp/compiler/Usage.class.php +++ b/src/main/php/xp/compiler/Usage.class.php @@ -1,29 +1,14 @@ packageContents($package) as $item) { - if (0 === substr_compare($item, \xp::CLASS_FILE_EXT, $offset)) { - yield $cl->loadClass($package.'.'.substr($item, 0, $offset)); - } - } - } - /** @return int */ public static function main(array $args) { Console::$err->writeLine('Usage: xp compile []'); @@ -32,26 +17,26 @@ public static function main(array $args) { public $byLoader= []; public function add($t, $active= false) { - $this->byLoader[$t->getClassLoader()->toString()][$t->getName()]= $active; + $this->byLoader[$t->classLoader()->toString()][$t->name()]= $active; } }; $emitter= Emitter::forRuntime(self::RUNTIME.':'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION); - foreach (self::classesIn('lang.ast.emit') as $class) { - if ($class->isSubclassOf(Emitter::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { - $impl->add($class, $class->equals($emitter)); + foreach ((new Package('lang.ast.emit'))->types() as $type) { + if ($type->is(Emitter::class) && !$type->modifiers()->isAbstract()) { + $impl->add($type, $type->class()->equals($emitter)); } } $language= Language::named(strtoupper(self::RUNTIME)); - foreach (self::classesIn('lang.ast.syntax') as $class) { - if ($class->isSubclassOf(Language::class) && !(MODIFIER_ABSTRACT & $class->getModifiers())) { - $impl->add($class, $class->isInstance($language)); + foreach ((new Package('lang.ast.syntax'))->types() as $type) { + if ($type->is(Language::class) && !$type->modifiers()->isAbstract()) { + $impl->add($type, $type->isInstance($language)); } } foreach ($language->extensions() as $extension) { - $impl->add(typeof($extension), 'true'); + $impl->add(Reflection::type($extension), true); } // Show implementations sorted by class loader From 3c70fb75122758d87b938db83ebfd7830a7da830 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 30 Jul 2023 12:18:40 +0200 Subject: [PATCH 793/926] Document refactoring --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index d8deb3a0..24175938 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Refactored code base to use the new reflection library instead of + the *Package* class from `lang.reflect`. See xp-framework/rfc#338 + (@thekid) + ## 8.15.1 / 2023-07-16 * Fixed parent constructors not being invoked when using non-constant From f6002438d55d1f988defe2c7c956bc1fc1090f39 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Oct 2023 19:13:05 +0200 Subject: [PATCH 794/926] Transform multiple nodes without creating statements --- src/main/php/lang/ast/Emitter.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 78cb79d5..987a532f 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -148,9 +148,9 @@ public function emitOne($result, $node) { $this->{'emit'.$r->kind}($result, $r); return; } else if ($r) { - foreach ($r as $n) { + foreach ($r as $s => $n) { $this->{'emit'.$n->kind}($result, $n); - $result->out->write(';'); + null === $s || $result->out->write(';'); } return; } From 3ccf3a429a175e2b5ab580b46fc8bd0ef1f9c019 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Oct 2023 20:39:45 +0200 Subject: [PATCH 795/926] Release 8.16.0 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 24175938..875ccff0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.16.0 / 2023-10-01 + +* Merged PR #175_ Transform multiple nodes without creating statements + (@thekid) * Refactored code base to use the new reflection library instead of the *Package* class from `lang.reflect`. See xp-framework/rfc#338 (@thekid) From 2055428f42dc541c371383872a9cb9a0ef9a34a9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Oct 2023 20:40:41 +0200 Subject: [PATCH 796/926] QA: Fix typo [skip ci] --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 875ccff0..dc932427 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,7 +5,7 @@ XP Compiler ChangeLog ## 8.16.0 / 2023-10-01 -* Merged PR #175_ Transform multiple nodes without creating statements +* Merged PR #175: Transform multiple nodes without creating statements (@thekid) * Refactored code base to use the new reflection library instead of the *Package* class from `lang.reflect`. See xp-framework/rfc#338 From bdc638ad2ceaa55b57be360c878eef90d260b4d8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Oct 2023 10:37:33 +0200 Subject: [PATCH 797/926] Prevent lambda parameters bleeding into locals Fixes #176 --- src/main/php/lang/ast/emit/PHP.class.php | 5 +++++ .../ast/unittest/emit/LambdasTest.class.php | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d1af26ce..9d003dd4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -365,10 +365,15 @@ protected function emitClosure($result, $closure) { } protected function emitLambda($result, $lambda) { + $result->stack[]= $result->locals; + $result->locals= []; + $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); $result->out->write('=>'); $this->emitOne($result, $lambda->body); + + $result->locals= array_pop($result->stack); } protected function emitEnumCase($result, $case) { diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 93964962..3ddee13c 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -225,4 +225,22 @@ public function run() { } }'); } + + #[Test] + public function issue_176() { + $r= $this->run('class %T { + public function run(iterable $records) { + $nonNull= fn($record) => null !== $record; + $process= fn($records, $filter) => { + foreach ($records as $record) { + if ($filter($record)) yield $record; + } + }; + + return $process($records, $nonNull); + } + }', [1, null, 2]); + + Assert::equals([1, 2], iterator_to_array($r)); + } } \ No newline at end of file From 3cf6944c56c4217b788414307159477782efb571 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Oct 2023 10:48:49 +0200 Subject: [PATCH 798/926] Carry locals over into lambda Part 2 of fix for #176 --- src/main/php/lang/ast/emit/PHP.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9d003dd4..3d56aaa6 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -366,7 +366,6 @@ protected function emitClosure($result, $closure) { protected function emitLambda($result, $lambda) { $result->stack[]= $result->locals; - $result->locals= []; $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); From 6eb2953183d05698015fa1dafbd8b7f6f53a2a01 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Oct 2023 10:53:38 +0200 Subject: [PATCH 799/926] Remove Result::$stack --- src/main/php/lang/ast/emit/PHP.class.php | 20 ++++++++++---------- src/main/php/lang/ast/emit/Result.class.php | 1 - 2 files changed, 10 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 3d56aaa6..b8a3d07a 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -129,7 +129,7 @@ protected function enclose($result, $node, $signature, $static, $emit) { } unset($capture['this']); - $result->stack[]= $result->locals; + $locals= $result->locals; $result->locals= []; if ($signature) { $static ? $result->out->write('static function') : $result->out->write('function'); @@ -151,7 +151,7 @@ protected function enclose($result, $node, $signature, $static, $emit) { $result->out->write('{'); $emit($result, $node); $result->out->write('}'); - $result->locals= array_pop($result->stack); + $result->locals= $locals; } /** @@ -331,7 +331,7 @@ protected function emitSignature($result, $signature) { } protected function emitFunction($result, $function) { - $result->stack[]= $result->locals; + $locals= $result->locals; $result->locals= []; $result->out->write('function '.($function->signature->byref ? '&' : '').$function->name); @@ -341,11 +341,11 @@ protected function emitFunction($result, $function) { $this->emitAll($result, $function->body); $result->out->write('}'); - $result->locals= array_pop($result->stack); + $result->locals= $locals; } protected function emitClosure($result, $closure) { - $result->stack[]= $result->locals; + $locals= $result->locals; $result->locals= []; $closure->static ? $result->out->write('static function') : $result->out->write('function'); @@ -361,18 +361,18 @@ protected function emitClosure($result, $closure) { $this->emitAll($result, $closure->body); $result->out->write('}'); - $result->locals= array_pop($result->stack); + $result->locals= $locals; } protected function emitLambda($result, $lambda) { - $result->stack[]= $result->locals; + $locals= $result->locals; $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); $this->emitSignature($result, $lambda->signature); $result->out->write('=>'); $this->emitOne($result, $lambda->body); - $result->locals= array_pop($result->stack); + $result->locals= $locals; } protected function emitEnumCase($result, $case) { @@ -636,7 +636,7 @@ protected function emitProperty($result, $property) { } protected function emitMethod($result, $method) { - $result->stack[]= $result->locals; + $locals= $result->locals; $result->locals= ['this' => true]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', @@ -708,7 +708,7 @@ protected function emitMethod($result, $method) { $this->emitProperty($result, new Property(explode(' ', $param->promote), $param->name, $param->type)); } - $result->locals= array_pop($result->stack); + $result->locals= $locals; $result->codegen->scope[0]->meta[self::METHOD][$method->name]= $meta; } diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php index f83146c0..83a38911 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -8,7 +8,6 @@ class Result implements Closeable { public $out; public $codegen; public $locals= []; - public $stack= []; /** * Starts a result stream. From 8a4f99501e9b0ec3f0f034fbe45597978a6b8698 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Oct 2023 11:05:40 +0200 Subject: [PATCH 800/926] Release 8.17.0 --- ChangeLog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index dc932427..954c12ab 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.17.0 / 2023-10-03 + +* Merged PR #177: Remove Result::$stack. Use local variables for backing + up and restoring locals instead. Slight performance improvement. + (@thekid) +* Fixed issue #176: Lambda parameters bleeding into locals - @thekid + ## 8.16.0 / 2023-10-01 * Merged PR #175: Transform multiple nodes without creating statements From 2740c4cc647ef39ef2a02bf046242bff014a20b9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Oct 2023 08:43:44 +0200 Subject: [PATCH 801/926] Add test for line numbers --- .../unittest/emit/LineNumbersTest.class.php | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php new file mode 100755 index 00000000..0bf1c2c9 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php @@ -0,0 +1,123 @@ +run('class %T { + public function run() { + return __LINE__; + } + }'); + Assert::equals(3, $r); + } + + #[Test] + public function array() { + $r= $this->run('class %T { + public function run() { + return [ + __LINE__, + __LINE__, + ]; + } + }'); + Assert::equals([4, 5], $r); + } + + #[Test] + public function addition() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l) { + $this->lines[]= $l; + return 0; + } + + public function run() { + $r= + $this->line(__LINE__) + + $this->line(__LINE__) + ; + return $this->lines; + } + }'); + Assert::equals([11, 12], $r); + } + + #[Test] + public function chain() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l) { + $this->lines[]= $l; + return $this; + } + + public function run() { + return $this + ->line(__LINE__) + ->line(__LINE__) + ->lines + ; + } + }'); + Assert::equals([11, 12], $r); + } + + #[Test, Values([[true, 4], [false, 5]])] + public function ternary($arg, $line) { + $r= $this->run('class %T { + public function run($arg) { + return $arg + ? __LINE__ + : __LINE__ + ; + } + }', $arg); + Assert::equals($line, $r); + } + + #[Test] + public function binary() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l, $r) { + $this->lines[]= $l; + return $r; + } + + public function run() { + $r= ( + $this->line(__LINE__, true) && + $this->line(__LINE__, false) || + $this->line(__LINE__, true) + ); + return $this->lines; + } + }'); + Assert::equals([11, 12, 13], $r); + } + + #[Test] + public function arguments() { + $r= $this->run('class %T { + private function lines(... $lines) { + return $lines; + } + + public function run() { + return $this->lines( + __LINE__, + __LINE__, + ); + } + }'); + Assert::equals([8, 9], $r); + } +} \ No newline at end of file From fd0647238bfc22b48240f3cf970565480d7a04fe Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Oct 2023 08:45:13 +0200 Subject: [PATCH 802/926] Add link to xp-framework/ast#49 --- src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php index 0bf1c2c9..c26410c0 100755 --- a/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php @@ -2,6 +2,7 @@ use test\{Assert, Test, Values}; +/** @see https://github.com/xp-framework/ast/pull/49 */ class LineNumbersTest extends EmittingTest { #[Test] From e61f19cf301811d6898b748d4436816eb3d2d871 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 6 Oct 2023 08:47:20 +0200 Subject: [PATCH 803/926] Rename RFC [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e71ddf7d..8e2d5a58 100755 --- a/README.md +++ b/README.md @@ -112,5 +112,5 @@ See also -------- * [XP Compiler design](https://github.com/xp-framework/compiler/wiki/Compiler-design) -* [XP RFC #0299: Make XP compiler the TypeScript of PHP](https://github.com/xp-framework/rfc/issues/299) +* [XP RFC #0299: Make XP compiler the Babel of PHP](https://github.com/xp-framework/rfc/issues/299) * [XP RFC #0327: Compile-time metaprogramming](https://github.com/xp-framework/rfc/issues/327) \ No newline at end of file From 2c8b56092d742f808c28b74337312c396a86ed94 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 10 Oct 2023 19:43:46 +0200 Subject: [PATCH 804/926] Run tests with PHP 8.4 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05af35d0..e5424b06 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions == '8.3' }} + continue-on-error: ${{ matrix.php-versions >= '8.3' }} strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] os: [ubuntu-latest, windows-latest] steps: From da372e9b070d7ca8c293b1182c248731f9808679 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 20 Nov 2023 21:29:48 +0100 Subject: [PATCH 805/926] Upgrade XP runners --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5424b06..35d8dba2 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.6.2.sh > xp-run && + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.8.0.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth From 01d0743fccf8a635f45d622c4b40c68d4cd17511 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Dec 2023 10:01:14 +0100 Subject: [PATCH 806/926] Only allow PHP >= 8.4 to fail PHP 8.3 has been released: https://externals.io/message/121782 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35d8dba2..48a56391 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions >= '8.3' }} + continue-on-error: ${{ matrix.php-versions >= '8.4' }} strategy: fail-fast: false matrix: From 5111675d119b3f1b04be99b51bba56a301cc14eb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Jan 2024 14:55:14 +0100 Subject: [PATCH 807/926] Fix emitting captures and return types for closures --- ChangeLog.md | 2 ++ src/main/php/lang/ast/emit/PHP.class.php | 17 +++++++++-------- .../ast/unittest/emit/LambdasTest.class.php | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 954c12ab..33614030 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed emitting captures and return types for closures - @thekid + ## 8.17.0 / 2023-10-03 * Merged PR #177: Remove Result::$stack. Use local variables for backing diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index b8a3d07a..fe08e57e 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -317,7 +317,7 @@ protected function emitParameter($result, $parameter) { $result->locals[$parameter->name]= true; } - protected function emitSignature($result, $signature) { + protected function emitSignature($result, $signature, $use= null) { $result->out->write('('); foreach ($signature->parameters as $i => $parameter) { if ($i++) $result->out->write(','); @@ -325,6 +325,13 @@ protected function emitSignature($result, $signature) { } $result->out->write(')'); + if ($use) { + $result->out->write(' use('.implode(',', $use).') '); + foreach ($use as $variable) { + $result->locals[substr($variable, 1)]= true; + } + } + if ($signature->returns && $t= $this->literal($signature->returns)) { $result->out->write(':'.$t); } @@ -349,14 +356,8 @@ protected function emitClosure($result, $closure) { $result->locals= []; $closure->static ? $result->out->write('static function') : $result->out->write('function'); - $this->emitSignature($result, $closure->signature); + $this->emitSignature($result, $closure->signature, $closure->use); - if ($closure->use) { - $result->out->write(' use('.implode(',', $closure->use).') '); - foreach ($closure->use as $variable) { - $result->locals[substr($variable, 1)]= true; - } - } $result->out->write('{'); $this->emitAll($result, $closure->body); $result->out->write('}'); diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 3ddee13c..013a02f1 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -243,4 +243,18 @@ public function run(iterable $records) { Assert::equals([1, 2], iterator_to_array($r)); } + + #[Test] + public function use_with_return_type() { + $r= $this->run('class %T { + public function run() { + $local= 1; + return function() use($local): int { + return $local; + }; + } + }'); + + Assert::equals(1, $r()); + } } \ No newline at end of file From fc5247e964a6421fe39af77f1897ab04e386cf41 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 6 Jan 2024 14:56:22 +0100 Subject: [PATCH 808/926] Release 8.17.1 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 33614030..cbad2d6a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.17.1 / 2024-01-06 + * Fixed emitting captures and return types for closures - @thekid ## 8.17.0 / 2023-10-03 From 8584d20b52e67b91dcfba71eed498a5b155318b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Feb 2024 12:36:42 +0100 Subject: [PATCH 809/926] 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 2464facda21be94e3316343b8ad0f52343224d0e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Mar 2024 09:59:20 +0100 Subject: [PATCH 810/926] Add test for `::class` --- .../php/lang/ast/unittest/emit/MembersTest.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 834b1740..3efb52a5 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -236,6 +236,16 @@ public function run() { Assert::equals(['Test'], $r); } + #[Test] + public function magic_class_constant() { + $t= $this->type('class %T { + public function run() { + return self::class; + } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } + #[Test, Values(['variable', 'invocation', 'array'])] public function class_on_objects($via) { $t= $this->declare('class %T { From b6c1221e7f040f0e4c2bdefcf27d1e3437999317 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Mar 2024 10:27:45 +0100 Subject: [PATCH 811/926] Test keywords being used as method names --- .../lang/ast/unittest/emit/MembersTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 3efb52a5..348aa566 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -412,4 +412,17 @@ class %T { public function run(): array { return [$this]; } } '); Assert::equals(new ArrayType($t->class()), $t->method('run')->returns()->type()); } + + #[Test, Values(['namespace', 'class', 'new', 'use', 'interface', 'trait', 'enum'])] + public function keyword_used_as_method_name($keyword) { + $r= $this->run('class %T { + private static function '.$keyword.'() { return "Test"; } + + public function run() { + return self::'.$keyword.'(); + } + }'); + + Assert::equals('Test', $r); + } } \ No newline at end of file From 5929c83e8c6de220de129c1e852e152f001bf443 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 16 Mar 2024 17:57:13 +0100 Subject: [PATCH 812/926] 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 813/926] 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 814/926] 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 7464265ea933293a7182ac0a5e6941fd2a0ae778 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 19:32:21 +0100 Subject: [PATCH 815/926] Use `?T` syntax for nullable parameters --- .../lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- src/test/php/lang/ast/unittest/emit/ParameterTest.class.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 1464e3f8..26481133 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -93,7 +93,7 @@ public function __construct(private string... $in) { } #[Test] public function can_be_mixed_with_normal_arguments() { $t= $this->declare('class %T { - public function __construct(public string $name, string $initial= null) { + public function __construct(public string $name, ?string $initial= null) { if (null !== $initial) $this->name.= " ".$initial."."; } }'); diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index f2fc7f1d..fc836c0c 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -50,7 +50,7 @@ public function value_typed() { #[Test] public function value_type_with_null() { - Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('Value $param= null')->constraint()->type()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param= null')->constraint()->type()); } #[Test] @@ -65,7 +65,7 @@ public function string_typed() { #[Test] public function string_typed_with_null() { - Assert::equals($this->nullable(Primitive::$STRING), $this->param('string $param= null')->constraint()->type()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param= null')->constraint()->type()); } #[Test] From 378ad38addbe63f2db98f71d7b3fc49fc5d3d866 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 19:53:53 +0100 Subject: [PATCH 816/926] Fix "implicitely nullable type" warnings See https://wiki.php.net/rfc/deprecate-implicitly-nullable-types --- ChangeLog.md | 7 +++++++ src/main/php/lang/ast/emit/PHP.class.php | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index cbad2d6a..845a5ca5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,13 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 8.17.2 / 2024-03-23 + +* Fixed *implicitely nullable type* warnings for parameters with non- + constant expressions (e.g. `$param= new Handle(0)`) in PHP 8.4, see + https://wiki.php.net/rfc/deprecate-implicitly-nullable-types + (@thekid) + ## 8.17.1 / 2024-01-06 * Fixed emitting captures and return types for closures - @thekid diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index fe08e57e..1a73e1ae 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -297,24 +297,35 @@ protected function emitArray($result, $array) { } protected function emitParameter($result, $parameter) { + $result->locals[$parameter->name]= true; $parameter->annotations && $this->emitOne($result, $parameter->annotations); - if ($parameter->type && $t= $this->literal($parameter->type)) { - $result->out->write($t.' '); + + // If we have a non-constant default and a type, emit a nullable type hint + // to prevent "implicitely nullable type" warnings being raised. See here: + // https://wiki.php.net/rfc/deprecate-implicitly-nullable-types + $type= $parameter->type; + if ($parameter->default) { + $const= $this->isConstant($result, $parameter->default); + if ($type && !$const && !$type instanceof IsNullable) { + $type= new IsNullable($parameter->type); + } } + if ($type && $t= $this->literal($type)) $result->out->write($t.' '); + if ($parameter->variadic) { $result->out->write('... $'.$parameter->name); } else { $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); } + if ($parameter->default) { - if ($this->isConstant($result, $parameter->default)) { + if ($const) { $result->out->write('='); $this->emitOne($result, $parameter->default); } else { $result->out->write('=null'); } } - $result->locals[$parameter->name]= true; } protected function emitSignature($result, $signature, $use= null) { From 53b2a70bb20adb57640d879760b22a3e18cb1f3e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 20:56:20 +0100 Subject: [PATCH 817/926] Drop PHP < 7.4 --- .github/workflows/ci.yml | 6 +++--- composer.json | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48a56391..495fe429 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] os: [ubuntu-latest, windows-latest] steps: @@ -24,7 +24,7 @@ jobs: run: git config --system core.autocrlf false; git config --system core.eol lf - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up PHP ${{ matrix.php-versions }} uses: shivammathur/setup-php@v2 @@ -40,7 +40,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/composer.json b/composer.json index feee80ed..303b7c68 100755 --- a/composer.json +++ b/composer.json @@ -6,13 +6,13 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^11.6 | ^10.16", - "xp-framework/reflection": "^2.13", - "xp-framework/ast": "^10.1", - "php" : ">=7.0.0" + "xp-framework/core": "^12.0 | ^11.6 | ^10.16", + "xp-framework/reflection": "^3.0 | ^2.13", + "xp-framework/ast": "^11.0 | ^10.1", + "php" : ">=7.4.0" }, "require-dev" : { - "xp-framework/test": "^1.5" + "xp-framework/test": "^2.0 | ^1.5" }, "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { From 8545c0b526f3333f29d737a372f0f05a7e964d69 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 21:01:29 +0100 Subject: [PATCH 818/926] Use utility class existing in both XP 11 and XP 12 --- .../ast/unittest/emit/AnonymousClassTest.class.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 6c769eb6..88513341 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -2,7 +2,7 @@ use lang\{Runnable, Reflection}; use test\{Assert, Test}; -use util\AbstractDeferredInvokationHandler; +use util\Binford; /** * Anonymous class support @@ -28,14 +28,15 @@ public function id() { return "test"; } public function extending_base_class() { $r= $this->run('class %T { public function run() { - return new class() extends \\util\\AbstractDeferredInvokationHandler { - public function initialize() { - // TBI + return new class() extends \\util\\Binford { + public function more(int $factor): self { + $this->poweredBy*= $factor; + return $this; } }; } }'); - Assert::instance(AbstractDeferredInvokationHandler::class, $r); + Assert::instance(Binford::class, $r); } #[Test] From 070e728c296796e8a21307b191680168c432b9f5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 21:24:44 +0100 Subject: [PATCH 819/926] Remove checks for no longer supported PHP versions --- .../php/lang/ast/unittest/emit/TypeDeclarationTest.class.php | 3 +-- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index 32401255..8accdf5a 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -2,7 +2,6 @@ use lang\reflection\Kind; use lang\{XPClass, Type}; -use test\verify\Runtime; use test\{Assert, Test, Values}; class TypeDeclarationTest extends EmittingTest { @@ -69,7 +68,7 @@ public function interface_type_with_method() { ); } - #[Test, Values(['public', 'private', 'protected']), Runtime(php: '>=7.1.0')] + #[Test, Values(['public', 'private', 'protected'])] public function constant($modifiers) { $c= $this->declare('class %T { '.$modifiers.' const test = 1; }')->constant('test'); Assert::equals( diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 14819c2c..7cccd040 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -3,7 +3,6 @@ use io\{File, Files, Folder}; use lang\ast\CompilingClassLoader; use lang\{ClassFormatException, ClassLoader, ClassNotFoundException, ElementNotFoundException, Environment}; -use test\verify\Runtime; use test\{Action, Assert, Expect, Test, Values}; class CompilingClassLoaderTest { @@ -138,7 +137,7 @@ public function load_class_with_syntax_errors() { }); } - #[Test, Runtime(php: '>=7.3'), Expect(class: ClassFormatException::class, message: '/Compiler error: Class .+ not found/')] + #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Class .+ not found/')] public function load_class_with_non_existant_parent() { $code= "compile(['Orphan' => $code], function($loader, $types) { From 846d6bb5e08c8140e874f4db1dcfadd4fbf28735 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 21:26:42 +0100 Subject: [PATCH 820/926] Use [...] destructuring instead of list(...) --- src/main/php/lang/ast/Compiled.class.php | 2 +- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 82b7de6a..361c9585 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -39,7 +39,7 @@ private static function parse($lang, $in, $version, $out, $file) { * @param string $opened */ public function stream_open($path, $mode, $options, &$opened) { - list($version, $file)= explode('://', $path); + [$version, $file]= explode('://', $path); $stream= self::$source[$file][1]->getResourceAsStream($file); self::parse(self::$source[$file][0], $stream->in(), $version, $this, $file); $opened= $stream->getURI(); diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 7cccd040..54211ecb 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -35,7 +35,7 @@ private function tempFolder($structure) { * @return var */ private function compile($source, $callback) { - list($folder, $names)= $this->tempFolder($source); + [$folder, $names]= $this->tempFolder($source); $cl= ClassLoader::registerPath($folder->path); try { @@ -204,7 +204,7 @@ public function loading_non_existant_resource_as_stream() { #[Test] public function ignores_autoload_and_xp_entry() { - list($folder, $names)= $this->tempFolder([ + [$folder, $names]= $this->tempFolder([ '__xp' => ' ' ' Date: Sat, 23 Mar 2024 21:29:04 +0100 Subject: [PATCH 821/926] Use array unpacking instead of iterator_to_array() --- src/test/php/lang/ast/unittest/emit/LambdasTest.class.php | 2 +- .../lang/ast/unittest/emit/TypeDeclarationTest.class.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 013a02f1..26ad8fd9 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -241,7 +241,7 @@ public function run(iterable $records) { } }', [1, null, 2]); - Assert::equals([1, 2], iterator_to_array($r)); + Assert::equals([1, 2], [...$r]); } #[Test] diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index 8accdf5a..18fd7495 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -12,9 +12,9 @@ public function empty_type($kind) { Assert::equals( ['constants' => [], 'properties' => [], 'methods' => []], [ - 'constants' => iterator_to_array($t->constants()), - 'properties' => iterator_to_array($t->properties()), - 'methods' => iterator_to_array($t->methods()) + 'constants' => [...$t->constants()], + 'properties' => [...$t->properties()], + 'methods' => [...$t->methods()], ] ); } From d979d403d00e69e3127889a741f4e7ac1a4e4f6a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 21:37:40 +0100 Subject: [PATCH 822/926] Use arrow functions --- .../lang/ast/unittest/EmitterTest.class.php | 8 ++-- .../loader/CompilingClassLoaderTest.class.php | 42 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index d386d3b4..51c2f378 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -34,7 +34,7 @@ public function transformations_initially_empty() { #[Test] public function transform() { - $function= function($class) { return $class; }; + $function= fn($class) => $class; $fixture= $this->newEmitter(); $fixture->transform('class', $function); @@ -43,7 +43,7 @@ public function transform() { #[Test] public function remove() { - $first= function($codegen, $class) { return $class; }; + $first= fn($codegen, $class) => $class; $second= function($codegen, $class) { $class->annotations['author']= 'Test'; return $class; }; $fixture= $this->newEmitter(); @@ -55,7 +55,7 @@ public function remove() { #[Test] public function remove_unsets_empty_kind() { - $function= function($codegen, $class) { return $class; }; + $function= fn($codegen, $class) => $class; $fixture= $this->newEmitter(); $transformation= $fixture->transform('class', $function); @@ -108,7 +108,7 @@ public function transform_to_array() { #[Test] public function transform_to_null() { $fixture= $this->newEmitter(); - $fixture->transform('variable', function($codegen, $var) { return null; }); + $fixture->transform('variable', fn($codegen, $var) => null); $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); Assert::equals('bytes()); diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 54211ecb..0c87bf48 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -68,9 +68,10 @@ public function hashcode() { #[Test] public function load_class() { - Assert::equals('Tests', $this->compile(['Tests' => 'loadClass($types['Tests'])->getSimpleName(); - })); + Assert::equals('Tests', $this->compile( + ['Tests' => ' $loader->loadClass($types['Tests'])->getSimpleName() + )); } #[Test] @@ -91,9 +92,10 @@ public function instanced_for_augmented() { #[Test] public function package_contents() { - $contents= $this->compile(['Tests' => 'packageContents(strstr($types['Tests'], '.', true)); - }); + $contents= $this->compile( + ['Tests' => ' $loader->packageContents(strstr($types['Tests'], '.', true)) + ); Assert::equals(['Tests'.\xp::CLASS_FILE_EXT], $contents); } @@ -106,8 +108,8 @@ public function load_dependencies() { 'Feature' => 'compile($source, function($loader, $types) { return $loader->loadClass($types['Child']); }); - $n= function($c) { return $c->getSimpleName(); }; + $c= $this->compile($source, fn($loader, $types) => $loader->loadClass($types['Child'])); + $n= fn($class) => $class->getSimpleName(); Assert::equals( ['Child', 'Base', ['Impl'], ['Feature']], [$n($c), $n($c->getParentClass()), array_map($n, $c->getInterfaces()), array_map($n, $c->getTraits())] @@ -116,33 +118,31 @@ public function load_dependencies() { #[Test] public function load_class_bytes() { - $code= $this->compile(['Tests' => 'loadClassBytes($types['Tests']); - }); + $code= $this->compile( + ['Tests' => ' $loader->loadClassBytes($types['Tests']) + ); Assert::matches('/<\?php .+ class Tests/', $code); } #[Test] public function load_uri() { - $class= $this->compile(['Tests' => 'loadUri($temp->path.strtr($types['Tests'], '.', DIRECTORY_SEPARATOR).CompilingClassLoader::EXTENSION); - }); + $class= $this->compile( + ['Tests' => ' $loader->loadUri($temp->path.strtr($types['Tests'], '.', DIRECTORY_SEPARATOR).CompilingClassLoader::EXTENSION) + ); Assert::equals('Tests', $class->getSimpleName()); } #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Expected "type name", have .+/')] public function load_class_with_syntax_errors() { - $this->compile(['Errors' => "loadClass($types['Errors']); - }); + $this->compile(['Errors' => " $loader->loadClass($types['Errors'])); } #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Class .+ not found/')] public function load_class_with_non_existant_parent() { $code= "compile(['Orphan' => $code], function($loader, $types) { - return $loader->loadClass($types['Orphan']); - }); + $this->compile(['Orphan' => $code], fn($loader, $types) => $loader->loadClass($types['Orphan'])); } #[Test] @@ -152,7 +152,7 @@ public function trigger() { trigger_error("Test"); } }']; - $t= $this->compile($source, function($loader, $types) { return $loader->loadClass($types['Triggers']); }); + $t= $this->compile($source, fn($loader, $types) => $loader->loadClass($types['Triggers'])); $t->newInstance()->trigger(); Assert::notEquals(false, strpos( From 387e4b875966db896a85c3751bf74da7a8eb340b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 21:45:34 +0100 Subject: [PATCH 823/926] Use PHP 8 versions --- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 0c87bf48..02310131 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -58,12 +58,12 @@ public function supports_php($version) { #[Test] public function string_representation() { - Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:7.0.0')->toString()); + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:8.0.0')->toString()); } #[Test] public function hashcode() { - Assert::equals('CPHP70+lang.ast.emit.php.XpMeta', CompilingClassLoader::instanceFor('php:7.0.0')->hashCode()); + Assert::equals('CPHP80+lang.ast.emit.php.XpMeta', CompilingClassLoader::instanceFor('php:8.0.0')->hashCode()); } #[Test] From 84ba99c75bd1845ea79cc2f278254d168d47b937 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 22:19:15 +0100 Subject: [PATCH 824/926] Remove PHP 7.0 - 7.3 emitters --- .../ForeachDestructuringAsStatement.class.php | 41 ------ .../ast/emit/OmitConstModifiers.class.php | 24 --- .../lang/ast/emit/OmitPropertyTypes.class.php | 18 --- .../lang/ast/emit/OmitReturnTypes.class.php | 18 --- src/main/php/lang/ast/emit/PHP70.class.php | 139 ------------------ src/main/php/lang/ast/emit/PHP71.class.php | 60 -------- src/main/php/lang/ast/emit/PHP72.class.php | 59 -------- src/main/php/lang/ast/emit/PHP73.class.php | 71 --------- .../ast/emit/RewriteAssignments.class.php | 90 ------------ .../emit/RewriteLambdaExpressions.class.php | 23 --- .../lang/ast/emit/RewriteMultiCatch.class.php | 26 ---- .../ast/unittest/TypeLiteralsTest.class.php | 71 +-------- .../emit/OmitConstModifiersTest.class.php | 34 ----- .../ast/unittest/emit/OmitTypesTest.class.php | 19 --- .../RewriteLambdaExpressionsTest.class.php | 31 ---- .../emit/RewriteMultiCatchTest.class.php | 39 ----- .../loader/CompilingClassLoaderTest.class.php | 2 +- 17 files changed, 3 insertions(+), 762 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php delete mode 100755 src/main/php/lang/ast/emit/OmitConstModifiers.class.php delete mode 100755 src/main/php/lang/ast/emit/OmitPropertyTypes.class.php delete mode 100755 src/main/php/lang/ast/emit/OmitReturnTypes.class.php delete mode 100755 src/main/php/lang/ast/emit/PHP70.class.php delete mode 100755 src/main/php/lang/ast/emit/PHP71.class.php delete mode 100755 src/main/php/lang/ast/emit/PHP72.class.php delete mode 100755 src/main/php/lang/ast/emit/PHP73.class.php delete mode 100755 src/main/php/lang/ast/emit/RewriteAssignments.class.php delete mode 100755 src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php delete mode 100755 src/main/php/lang/ast/emit/RewriteMultiCatch.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php diff --git a/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php b/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php deleted file mode 100755 index 46c24523..00000000 --- a/src/main/php/lang/ast/emit/ForeachDestructuringAsStatement.class.php +++ /dev/null @@ -1,41 +0,0 @@ -out->write('foreach ('); - $this->emitOne($result, $foreach->expression); - $result->out->write(' as '); - if ($foreach->key) { - $this->emitOne($result, $foreach->key); - $result->out->write(' => '); - } - - if ('array' === $foreach->value->kind) { - $t= $result->temp(); - $result->out->write('&'.$t.') {'); - $this->rewriteDestructuring($result, new Assignment($foreach->value, '=', new Variable(substr($t, 1)))); - $result->out->write(';'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); - } else { - $this->emitOne($result, $foreach->value); - $result->out->write(') {'); - $this->emitAll($result, $foreach->body); - $result->out->write('}'); - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php deleted file mode 100755 index cab1aacd..00000000 --- a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php +++ /dev/null @@ -1,24 +0,0 @@ -codegen->scope[0]->meta[self::CONSTANT][$const->name]= [ - DETAIL_RETURNS => $const->type ? $const->type->name() : 'var', - DETAIL_ANNOTATIONS => $const->annotations, - DETAIL_COMMENT => $const->comment, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - - $result->out->write('const '.$const->name.'='); - $this->emitOne($result, $const->expression); - $result->out->write(';'); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitPropertyTypes.class.php b/src/main/php/lang/ast/emit/OmitPropertyTypes.class.php deleted file mode 100755 index e2354be3..00000000 --- a/src/main/php/lang/ast/emit/OmitPropertyTypes.class.php +++ /dev/null @@ -1,18 +0,0 @@ - literal mappings */ - public function __construct() { - $this->literals= [ - IsFunction::class => function($t) { return 'callable'; }, - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { return null; }, - IsUnion::class => function($t) { return null; }, - IsIntersection::class => function($t) { return null; }, - IsLiteral::class => function($t) { - static $rewrite= [ - 'object' => 1, - 'void' => 1, - 'iterable' => 1, - 'mixed' => 1, - 'null' => 1, - 'never' => 1, - 'true' => 'bool', - 'false' => 'bool', - ]; - - $l= $t->literal(); - return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - }, - IsGeneric::class => function($t) { return null; } - ]; - } - - protected function emitCallable($result, $callable) { - $t= $result->temp(); - $result->out->write('(is_callable('.$t.'='); - if ($callable->expression instanceof Literal) { - - // Rewrite f() => "f" - $result->out->write('"'.trim($callable->expression->expression, '"\'').'"'); - } else if ($callable->expression instanceof InstanceExpression) { - - // Rewrite $this->f => [$this, "f"] - $result->out->write('['); - $this->emitOne($result, $callable->expression->expression); - if ($callable->expression->member instanceof Literal) { - $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); - } else if ($callable->expression->member instanceof Expression) { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member->inline); - } else { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member); - } - $result->out->write(']'); - } else if ($callable->expression instanceof ScopeExpression) { - - // Rewrite self::f => [self::class, "f"] - $result->out->write('['); - if ($callable->expression->type instanceof Node) { - $this->emitOne($result, $callable->expression->type); - } else { - $result->out->write($callable->expression->type.'::class'); - } - if ($callable->expression->member instanceof Literal) { - $result->out->write(',"'.trim($callable->expression->member, '"\'').'"'); - } else if ($callable->expression->member instanceof Expression) { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member->inline); - } else { - $result->out->write(','); - $this->emitOne($result, $callable->expression->member); - } - $result->out->write(']'); - } else { - - // Emit other expressions as-is - $this->emitOne($result, $callable->expression); - } - - // Emit equivalent of Closure::fromCallable() which doesn't exist until PHP 7.1 - $a= $result->temp(); - $result->out->write(')?function(...'.$a.') use('.$t.') { return '.$t.'(...'.$a.'); }:'); - $result->out->write('(function() { throw new \Error("Given argument is not callable"); })())'); - } - - protected function emitAssignment($result, $assignment) { - if ('??=' === $assignment->operator) { - - // Rewrite null-coalesce operator - $this->emitAssign($result, $assignment->variable); - $result->out->write('??'); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->expression); - return; - } else if ('array' === $assignment->variable->kind) { - - // Rewrite destructuring unless assignment consists only of variables - foreach ($assignment->variable->values as $pair) { - if (null === $pair[0] && (null === $pair[1] || $pair[1] instanceof Variable)) continue; - return $this->rewriteDestructuring($result, $assignment); - } - } - - return parent::emitAssignment($result, $assignment); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php deleted file mode 100755 index ecf1ea1c..00000000 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ /dev/null @@ -1,60 +0,0 @@ - literal mappings */ - public function __construct() { - $this->literals= [ - IsFunction::class => function($t) { return 'callable'; }, - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsIntersection::class => function($t) { return null; }, - IsLiteral::class => function($t) { - static $rewrite= [ - 'object' => 1, - 'mixed' => 1, - 'null' => 1, - 'never' => 'void', - 'true' => 'bool', - 'false' => 'bool', - ]; - - $l= $t->literal(); - return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - }, - IsGeneric::class => function($t) { return null; } - ]; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php deleted file mode 100755 index 21ef7b23..00000000 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ /dev/null @@ -1,59 +0,0 @@ - literal mappings */ - public function __construct() { - $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsIntersection::class => function($t) { return null; }, - IsLiteral::class => function($t) { - static $rewrite= [ - 'mixed' => 1, - 'null' => 1, - 'never' => 'void', - 'true' => 'bool', - 'false' => 'bool', - ]; - - $l= $t->literal(); - return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - }, - IsGeneric::class => function($t) { return null; } - ]; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php deleted file mode 100755 index 8b0c0fd9..00000000 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ /dev/null @@ -1,71 +0,0 @@ - literal mappings */ - public function __construct() { - $this->literals= [ - IsArray::class => function($t) { return 'array'; }, - IsMap::class => function($t) { return 'array'; }, - IsFunction::class => function($t) { return 'callable'; }, - IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, - IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, - IsUnion::class => function($t) { return null; }, - IsIntersection::class => function($t) { return null; }, - IsLiteral::class => function($t) { - static $rewrite= [ - 'mixed' => 1, - 'null' => 1, - 'never' => 'void', - 'true' => 'bool', - 'false' => 'bool', - ]; - - $l= $t->literal(); - return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; - }, - IsGeneric::class => function($t) { return null; } - ]; - } - - protected function emitAssignment($result, $assignment) { - if ('??=' === $assignment->operator) { - $this->emitAssign($result, $assignment->variable); - $result->out->write('??'); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->expression); - } else { - parent::emitAssignment($result, $assignment); - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteAssignments.class.php b/src/main/php/lang/ast/emit/RewriteAssignments.class.php deleted file mode 100755 index 634e19b4..00000000 --- a/src/main/php/lang/ast/emit/RewriteAssignments.class.php +++ /dev/null @@ -1,90 +0,0 @@ -temp(); - $result->out->write('is_array('.$t.'='); - - // Create reference to right-hand if possible - $r= $assignment->expression; - if ( - ($r instanceof Variable) || - ($r instanceof InstanceExpression && $r->member instanceof Literal) || - ($r instanceof ScopeExpression && $r->member instanceof Variable) - ) { - $result->out->write('&'); - } - - $temp= new Variable(substr($t, 1)); - $this->emitOne($result, $assignment->expression); - $result->out->write(')?['); - foreach ($assignment->variable->values as $i => $pair) { - if (null === $pair[1]) { - $result->out->write('null,'); - continue; - } - - // Assign by reference - $value= new OffsetExpression($temp, $pair[0] ?? new Literal($i)); - if ($pair[1] instanceof UnaryExpression) { - $this->emitAssignment($result, new Assignment($pair[1]->expression, '=&', $value)); - } else { - $this->emitAssignment($result, new Assignment($pair[1], '=', $value)); - } - $result->out->write(','); - } - - $null= new Literal('null'); - $result->out->write(']:(['); - foreach ($assignment->variable->values as $pair) { - if (null === $pair[1]) { - continue; - } else if ($pair[1] instanceof UnaryExpression) { - $this->emitAssignment($result, new Assignment($pair[1]->expression, '=', $null)); - } else if ($pair[1]) { - $this->emitAssignment($result, new Assignment($pair[1], '=', $null)); - } - $result->out->write(','); - } - $result->out->write(']?'.$t.':null)'); - } - - protected function emitAssignment($result, $assignment) { - if ('??=' === $assignment->operator) { - - // Rewrite null-coalesce operator - $this->emitAssign($result, $assignment->variable); - $result->out->write('??'); - $this->emitOne($result, $assignment->variable); - $result->out->write('='); - $this->emitOne($result, $assignment->expression); - return; - } else if ('array' === $assignment->variable->kind) { - - // Rewrite destructuring unless assignment consists only of variables - foreach ($assignment->variable->values as $pair) { - if (null === $pair[1] || $pair[1] instanceof Variable) continue; - return $this->rewriteDestructuring($result, $assignment); - } - } - - return parent::emitAssignment($result, $assignment); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php deleted file mode 100755 index d1085011..00000000 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ /dev/null @@ -1,23 +0,0 @@ -enclose($result, $lambda, $lambda->signature, $lambda->static, function($result, $lambda) { - if ($lambda->body instanceof Block) { - $this->emitAll($result, $lambda->body->statements); - } else { - $result->out->write('return '); - $this->emitOne($result, $lambda->body); - $result->out->write(';'); - } - }); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php deleted file mode 100755 index 5088f5f6..00000000 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ /dev/null @@ -1,26 +0,0 @@ -variable ? '$'.$catch->variable : $result->temp(); - if (empty($catch->types)) { - $result->out->write('catch(\\Throwable '.$capture.') {'); - } else { - $last= array_pop($catch->types); - $label= $result->codegen->symbol(); - foreach ($catch->types as $type) { - $result->out->write('catch('.$type.' '.$capture.') { goto '.$label.'; }'); - } - $result->out->write('catch('.$last.' '.$capture.') { '.$label.':'); - } - - $this->emitAll($result, $catch->body); - $result->out->write('}'); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php index ce4d1204..999e24ce 100755 --- a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -17,54 +17,11 @@ private function base() { } /** - * PHP 7.0 - the base case - * - * @return iterable - */ - private function php70() { - yield from $this->base(); - yield [new IsLiteral('object'), null]; - yield [new IsLiteral('void'), null]; - yield [new IsLiteral('never'), null]; - yield [new IsLiteral('iterable'), null]; - yield [new IsLiteral('mixed'), null]; - yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), 'bool']; - yield [new IsLiteral('true'), 'bool']; - yield [new IsNullable(new IsLiteral('string')), null]; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; - yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; - } - - /** - * PHP 7.1 added `void` and `iterable` as well as support for nullable types - * - * @return iterable - */ - private function php71() { - yield from $this->base(); - yield [new IsLiteral('object'), null]; - yield [new IsLiteral('void'), 'void']; - yield [new IsLiteral('never'), 'void']; - yield [new IsLiteral('iterable'), 'iterable']; - yield [new IsLiteral('mixed'), null]; - yield [new IsLiteral('null'), null]; - yield [new IsLiteral('false'), 'bool']; - yield [new IsLiteral('true'), 'bool']; - yield [new IsNullable(new IsLiteral('string')), '?string']; - yield [new IsNullable(new IsLiteral('object')), null]; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; - yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; - yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; - } - - /** - * PHP 7.2 added `object` + * PHP 7.4 is the same as PHP 7.2 * * @return iterable */ - private function php72() { + private function php74() { yield from $this->base(); yield [new IsLiteral('object'), 'object']; yield [new IsLiteral('void'), 'void']; @@ -81,15 +38,6 @@ private function php72() { yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; } - /** - * PHP 7.4 is the same as PHP 7.2 - * - * @return iterable - */ - private function php74() { - yield from $this->php72(); - } - /** * PHP 8.0 added `mixed` and union types * @@ -156,21 +104,6 @@ private function php82() { yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; } - #[Test, Values(from: 'php70')] - public function php70_literals($type, $literal) { - Assert::equals($literal, (new PHP70())->literal($type)); - } - - #[Test, Values(from: 'php71')] - public function php71_literals($type, $literal) { - Assert::equals($literal, (new PHP71())->literal($type)); - } - - #[Test, Values(from: 'php72')] - public function php72_literals($type, $literal) { - Assert::equals($literal, (new PHP72())->literal($type)); - } - #[Test, Values(from: 'php74')] public function php74_literals($type, $literal) { Assert::equals($literal, (new PHP74())->literal($type)); diff --git a/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php b/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php deleted file mode 100755 index 393c6ac1..00000000 --- a/src/test/php/lang/ast/unittest/emit/OmitConstModifiersTest.class.php +++ /dev/null @@ -1,34 +0,0 @@ -type= new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1); - } - - #[Test] - public function omits_type() { - $const= new Constant([], 'TEST', new IsLiteral('string'), new Literal('"test"')); - Assert::equals('const TEST="test";', $this->emit($const, [$this->type])); - } - - #[Test] - public function omits_modifier() { - $const= new Constant(['private'], 'TEST', new IsLiteral('string'), new Literal('"test"')); - Assert::equals('const TEST="test";', $this->emit($const, [$this->type])); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php deleted file mode 100755 index 1d0c4408..00000000 --- a/src/test/php/lang/ast/unittest/emit/OmitTypesTest.class.php +++ /dev/null @@ -1,19 +0,0 @@ -propertyType(new IsLiteral('int'))); - } - - #[Test] - public function return_type() { - Assert::equals('', $this->returnType(new IsLiteral('int'))); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php deleted file mode 100755 index 96a98b79..00000000 --- a/src/test/php/lang/ast/unittest/emit/RewriteLambdaExpressionsTest.class.php +++ /dev/null @@ -1,31 +0,0 @@ -emit( - new LambdaExpression(new Signature([], null), new Literal('true')) - )); - } - - #[Test] - public function rewrites_fn_with_block_to_function() { - Assert::equals('function(){return false;}', $this->emit( - new LambdaExpression(new Signature([], null), new Block([ - new ReturnStatement(new Literal('false')) - ])) - )); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php deleted file mode 100755 index 89b3cb9a..00000000 --- a/src/test/php/lang/ast/unittest/emit/RewriteMultiCatchTest.class.php +++ /dev/null @@ -1,39 +0,0 @@ -emit(new TryStatement([], [new CatchStatement([], 't', [])], null)) - ); - } - - #[Test] - public function rewrites_catch_without_variable() { - Assert::equals( - 'try {}catch(\\Throwable $_0) {}', - $this->emit(new TryStatement([], [new CatchStatement([], null, [])], null)) - ); - } - - #[Test] - public function rewrites_catch_with_multiple_types_using_goto() { - Assert::equals( - 'try {}catch(\\Exception $t) { goto _0; }catch(\\Error $t) { _0:}', - $this->emit(new TryStatement([], [new CatchStatement(['\\Exception', '\\Error'], 't', [])], null)) - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 02310131..2c04bb04 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -51,7 +51,7 @@ public function can_create() { CompilingClassLoader::instanceFor(self::$runtime); } - #[Test, Values(['7.0.0', '7.0.1', '7.1.0', '7.2.0', '7.3.0', '7.4.0', '7.4.12', '8.0.0'])] + #[Test, Values(['7.4.0', '7.4.12', '8.0.0', '8.1.0', '8.2.0', '8.3.0', '8.4.0'])] public function supports_php($version) { CompilingClassLoader::instanceFor('php:'.$version); } From 0d5ec7207df5e2fc1f4f3343086d04638cf0209d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 22:20:58 +0100 Subject: [PATCH 825/926] Remove PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e2d5a58..0729767d 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ XP Compiler [![Build status on GitHub](https://github.com/xp-framework/compiler/workflows/Tests/badge.svg)](https://github.com/xp-framework/compiler/actions) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) -[![Requires PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.svg)](http://php.net/) +[![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/) [![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/) [![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) @@ -16,7 +16,7 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -The following code uses Hack language, PHP 8.3, PHP 8.2, 8.1, 8.0, PHP 7.4, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 7.0. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +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. ```php Date: Sat, 23 Mar 2024 22:26:07 +0100 Subject: [PATCH 826/926] Remove virtual typed properties for PHP 7.4 --- .../emit/php/VirtualPropertyTypes.class.php | 99 ---------- .../emit/VirtualPropertyTypesTest.class.php | 171 ------------------ 2 files changed, 270 deletions(-) delete mode 100755 src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php delete mode 100755 src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php diff --git a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php b/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php deleted file mode 100755 index 65a64e02..00000000 --- a/src/main/php/lang/ast/emit/php/VirtualPropertyTypes.class.php +++ /dev/null @@ -1,99 +0,0 @@ - MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY - ]; - - // Exclude properties w/o type, static and readonly properties - if (null === $property->type || in_array('static', $property->modifiers) || in_array('readonly', $property->modifiers)) { - return parent::emitProperty($result, $property); - } - - // Create virtual instance property implementing type coercion and checks - switch ($property->type->name()) { - case 'string': - $assign= 'if (is_scalar($value)) $this->__virtual["%1$s"]= (string)$value;'; - break; - case 'bool': - $assign= 'if (is_scalar($value)) $this->__virtual["%1$s"]= (bool)$value;'; - break; - case 'int': - $assign= 'if (is_numeric($value) || is_bool($value)) $this->__virtual["%1$s"]= (int)$value;'; - break; - case 'float': - $assign= 'if (is_numeric($value) || is_bool($value)) $this->__virtual["%1$s"]= (float)$value;'; - break; - default: - $assign= 'if (is("%2$s", $value)) $this->__virtual["%1$s"]= $value;'; - break; - } - - // Add visibility check for private and protected properties - if (in_array('private', $property->modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");' - ); - } else if (in_array('protected', $property->modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' - ); - } else { - $check= ''; - } - - $scope= $result->codegen->scope[0]; - $scope->virtual[$property->name]= [ - new Code(sprintf($check.'return $this->__virtual["%1$s"];', $property->name)), - new Code(sprintf( - $check.$assign. - 'else throw new \\TypeError("Cannot assign ".(is_object($value) ? get_class($value) : gettype($value))." to property ".__CLASS__."::\\$%1$s of type %2$s");', - $property->name, - $property->type->name() - )) - ]; - - // Initialize via constructor - if (isset($property->expression)) { - $scope->init['$this->'.$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/VirtualPropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php deleted file mode 100755 index 9f39d459..00000000 --- a/src/test/php/lang/ast/unittest/emit/VirtualPropertyTypesTest.class.php +++ /dev/null @@ -1,171 +0,0 @@ -declare('class %T { - private int $value; - }'); - - Assert::equals(Primitive::$INT, $t->property('value')->constraint()->type()); - } - - #[Test] - public function modifiers_available_via_reflection() { - $t= $this->declare('class %T { - private int $value; - }'); - - Assert::equals(MODIFIER_PRIVATE, $t->property('value')->modifiers()->bits()); - } - - #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] - public function cannot_read_private_field() { - $t= $this->declare('class %T { - private int $value; - }'); - - $t->newInstance()->value; - } - - #[Test, Expect(class: Error::class, message: '/Cannot access private property .+::\\$value/')] - public function cannot_write_private_field() { - $t= $this->declare('class %T { - private int $value; - }'); - - $t->newInstance()->value= 6100; - } - - #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] - public function cannot_read_protected_field() { - $t= $this->declare('class %T { - protected int $value; - }'); - - $t->newInstance()->value; - } - - #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+::\\$value/')] - public function cannot_write_protected_field() { - $t= $this->declare('class %T { - protected int $value; - }'); - - $t->newInstance()->value= 6100; - } - - #[Test] - public function can_access_protected_field_from_subclass() { - $t= $this->declare('class %T { - protected int $value; - }'); - $i= newinstance($t->literal(), [], [ - 'run' => function() { - $this->value= 6100; - return $this->value; - } - ]); - - Assert::equals(6100, $i->run()); - } - - #[Test] - public function initial_value_available_via_reflection() { - $t= $this->declare('class %T { - private int $value = 6100; - }'); - - Assert::equals(6100, $t->property('value')->get($t->newInstance(), $t)); - } - - #[Test, Values([[null], ['Test'], [[]]]), Expect(class: Error::class, message: '/property .+::\$value of type int/')] - public function type_checked_at_runtime($in) { - $this->run('class %T { - private int $value; - - public function run($arg) { - $this->value= $arg; - } - }', $in); - } - - #[Test] - public function value_type_test() { - $handle= new Handle(0); - $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { - private Handle $value; - - public function run($arg) { - $this->value= $arg; - return $this->value; - } - }', $handle); - - Assert::equals($handle, $r); - } - - #[Test, Values(['', 'Test', 1, 1.5, true, false])] - public function string_type_coercion($in) { - $r= $this->run('class %T { - private string $value; - - public function run($arg) { - $this->value= $arg; - return $this->value; - } - }', $in); - - Assert::equals((string)$in, $r); - } - - #[Test, Values(['', 'Test', 1, 1.5, true, false])] - public function bool_type_coercion($in) { - $r= $this->run('class %T { - private bool $value; - - public function run($arg) { - $this->value= $arg; - return $this->value; - } - }', $in); - - Assert::equals((bool)$in, $r); - } - - #[Test, Values(['1', '1.5', 1, 1.5, true, false])] - public function int_type_coercion($in) { - $r= $this->run('class %T { - private int $value; - - public function run($arg) { - $this->value= $arg; - return $this->value; - } - }', $in); - - Assert::equals((int)$in, $r); - } - - #[Test, Values(['1', '1.5', 1, 1.5, true, false])] - public function float_type_coercion($in) { - $r= $this->run('class %T { - private float $value; - - public function run($arg) { - $this->value= $arg; - return $this->value; - } - }', $in); - - Assert::equals((float)$in, $r); - } -} \ No newline at end of file From 944d07818058e0a13ba582553921ef18feaf081b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 22:54:54 +0100 Subject: [PATCH 827/926] Remove deprecated lang.ast.emit.GeneratedCode::lookup() --- ChangeLog.md | 5 ++ .../php/lang/ast/emit/GeneratedCode.class.php | 11 ----- .../lang/ast/unittest/ResultTest.class.php | 4 +- .../unittest/emit/GeneratedCodeTest.class.php | 49 +------------------ 4 files changed, 7 insertions(+), 62 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 845a5ca5..74cdf280 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.0.0 / ????-??-?? + +* Removed deprecated *lookup()* from `lang.ast.emit.GeneratedCode` + (@thekid) + ## 8.17.2 / 2024-03-23 * Fixed *implicitely nullable type* warnings for parameters with non- diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index e02acbe9..9e5b36bd 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -58,15 +58,4 @@ public function at($line) { public function temp() { return '$'.$this->codegen->symbol(); } - - /** - * Looks up a given type - * - * @deprecated Use `CodeGen::lookup()` instead! - * @param string $type - * @return lang.ast.emit.Type - */ - public function lookup($type) { - return $this->codegen->lookup($type); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 40c367c2..857e0dc6 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,9 +1,7 @@ ', $out->bytes()); } - #[Test] - public function lookup_self() { - $r= new GeneratedCode(new MemoryOutputStream()); - $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - - Assert::equals(new Declaration($context->type, $r->codegen), $r->lookup('self')); - } - - #[Test] - public function lookup_parent() { - $r= new GeneratedCode(new MemoryOutputStream()); - $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1))); - - Assert::equals(new Reflection(Value::class), $r->lookup('parent')); - } - - #[Test] - public function lookup_parent_without_parent() { - $r= new GeneratedCode(new MemoryOutputStream()); - $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - - Assert::null($r->lookup('parent')); - } - - #[Test] - public function lookup_named() { - $r= new GeneratedCode(new MemoryOutputStream()); - $context= $r->codegen->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); - - Assert::equals(new Declaration($context->type, $r->codegen), $r->lookup('\\T')); - } - - #[Test] - public function lookup_value_interface() { - $r= new GeneratedCode(new MemoryOutputStream()); - - Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); - } - - #[Test] - public function lookup_non_existant() { - $r= new GeneratedCode(new MemoryOutputStream()); - Assert::instance(Incomplete::class, $r->lookup('\\NotFound')); - } - #[Test] public function line_number_initially_1() { $r= new GeneratedCode(new MemoryOutputStream()); From f0968b34ae8cb1ca682129a9cff34474eec0d128 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 22:58:16 +0100 Subject: [PATCH 828/926] Remove PHP70, PHP71 & PHP72 emitters --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 0729767d..897ab5b2 100755 --- a/README.md +++ b/README.md @@ -87,9 +87,6 @@ $ xp compile Usage: xp compile [] @FileSystemCL<./vendor/xp-framework/compiler/src/main/php> -lang.ast.emit.PHP70 -lang.ast.emit.PHP71 -lang.ast.emit.PHP72 lang.ast.emit.PHP74 lang.ast.emit.PHP80 lang.ast.emit.PHP81 From cd308ad4404f96aae7a2ec38e53d3fb79b3572fc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 23:01:48 +0100 Subject: [PATCH 829/926] Drop PHP < 7.4 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 74cdf280..bf8bf9a5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,8 @@ XP Compiler ChangeLog ## 9.0.0 / ????-??-?? +* Merged PR #179: XP 12 compatibility, dropping PHP 7.0 - 7.3 support! + (@thekid) * Removed deprecated *lookup()* from `lang.ast.emit.GeneratedCode` (@thekid) From 4a9ebbeccfef1ee1b629c6ed4da60206afba88a1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 23:02:06 +0100 Subject: [PATCH 830/926] Emit short destructuring assignments --- src/main/php/lang/ast/emit/PHP.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1a73e1ae..ac3d93a4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -20,9 +20,9 @@ use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { - const PROPERTY = 0; - const METHOD = 1; - const CONSTANT = 2; + const PROPERTY= 0; + const METHOD = 1; + const CONSTANT= 2; protected $literals= []; @@ -770,7 +770,7 @@ protected function emitAssign($result, $target) { $result->out->write('$'.$target->pointer); $result->locals[$target->pointer]= true; } else if ($target instanceof ArrayLiteral) { - $result->out->write('list('); + $result->out->write('['); foreach ($target->values as $pair) { if ($pair[0]) { $this->emitOne($result, $pair[0]); @@ -781,7 +781,7 @@ protected function emitAssign($result, $target) { } $result->out->write(','); } - $result->out->write(')'); + $result->out->write(']'); } else { $this->emitOne($result, $target); } From f1bd59ed835fc4b6e8cad45e06d52ee8d2c83074 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 23:02:36 +0100 Subject: [PATCH 831/926] Reference lang.ast.unittest.CodeGenTest --- src/main/php/lang/ast/CodeGen.class.php | 1 + .../lang/ast/unittest/CodeGenTest.class.php | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/CodeGenTest.class.php diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php index c17c5add..4f1b936a 100755 --- a/src/main/php/lang/ast/CodeGen.class.php +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -2,6 +2,7 @@ use lang\ast\emit\{Reflection, Declaration, Incomplete}; +/** @test lang.ast.unittest.CodeGenTest */ class CodeGen { private $id= 0; public $scope= []; diff --git a/src/test/php/lang/ast/unittest/CodeGenTest.class.php b/src/test/php/lang/ast/unittest/CodeGenTest.class.php new file mode 100755 index 00000000..4e6abe3f --- /dev/null +++ b/src/test/php/lang/ast/unittest/CodeGenTest.class.php @@ -0,0 +1,62 @@ +enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::equals(new Declaration($context->type, $fixture), $fixture->lookup('self')); + } + + #[Test] + public function lookup_parent() { + $fixture= new CodeGen(); + $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1))); + + Assert::equals(new Reflection(Value::class), $fixture->lookup('parent')); + } + + #[Test] + public function lookup_parent_without_parent() { + $fixture= new CodeGen(); + $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::null($fixture->lookup('parent')); + } + + #[Test] + public function lookup_named() { + $fixture= new CodeGen(); + $context= $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::equals(new Declaration($context->type, $fixture), $fixture->lookup('\\T')); + } + + #[Test] + public function lookup_value_interface() { + $fixture= new CodeGen(); + + Assert::equals(new Reflection(Value::class), $fixture->lookup('\\lang\\Value')); + } + + #[Test] + public function lookup_non_existant() { + $fixture= new CodeGen(); + Assert::instance(Incomplete::class, $fixture->lookup('\\NotFound')); + } +} \ No newline at end of file From ee354ae32ac94493047efa3a9c82144ba03f5773 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 23:03:17 +0100 Subject: [PATCH 832/926] Remove deprecated `` syntax --- src/test/php/lang/ast/unittest/emit/EmittingTest.class.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index a565c35a..bfbc0eaf 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -62,7 +62,7 @@ protected function transform($type, $function) { */ protected function emit($code) { $name= 'E'.(self::$id++); - $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); + $tree= $this->language->parse(new Tokens(str_replace('%T', $name, $code), static::class))->tree(); $out= new MemoryOutputStream(); $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); @@ -91,8 +91,6 @@ protected function declare($code) { $name= 'T'.(self::$id++); if (strstr($code, '%T')) { $declaration= str_replace('%T', $name, $code); - } else if (strstr($code, '')) { - $declaration= str_replace('', $name, $code); // deprecated } else { $declaration= $code.' class '.$name.' { }'; } From 90791e1eeca96eafea3d05de99b1d9a4c45b574c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 Mar 2024 23:08:18 +0100 Subject: [PATCH 833/926] Release 9.0.0 --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index bf8bf9a5..a1b764d1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,7 +3,7 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -## 9.0.0 / ????-??-?? +## 9.0.0 / 2024-03-23 * Merged PR #179: XP 12 compatibility, dropping PHP 7.0 - 7.3 support! (@thekid) From 3b8235559102fba597c7675797795ecd0cc67fe9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Mar 2024 21:22:34 +0100 Subject: [PATCH 834/926] Add OperatorTest and include tests from NullCoalesceAssignmentTest --- .../emit/NullCoalesceAssignmentTest.class.php | 30 -------- .../ast/unittest/emit/OperatorTest.class.php | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 30 deletions(-) delete mode 100755 src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/OperatorTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php deleted file mode 100755 index 2a9eb74f..00000000 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceAssignmentTest.class.php +++ /dev/null @@ -1,30 +0,0 @@ -run('class %T { - public function run($arg) { - $arg??= true; - return $arg; - } - }', $value); - - Assert::equals($expected, $r); - } - - #[Test, Values([[[], true], [[null], true], [[false], false], [['Test'], 'Test']])] - public function fills_array_if_non_existant_or_null($value, $expected) { - $r= $this->run('class %T { - public function run($arg) { - $arg[0]??= true; - return $arg; - } - }', $value); - - Assert::equals($expected, $r[0]); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php new file mode 100755 index 00000000..0e1833b0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -0,0 +1,68 @@ +run('class %T { + public function run() { + $a= 1; + $a'.$op.' 2; + return $a; + } + }'); + + Assert::equals($expected, $r); + } + + #[Test] + public function destructuring() { + $r= $this->run('class %T { + public function run() { + [$a, $b]= explode("..", "A..B"); + return [$a, $b]; + } + }'); + + Assert::equals(['A', 'B'], $r); + } + + #[Test] + public function swap_variables() { + $r= $this->run('class %T { + public function run() { + $a= 1; $b= 2; + [$a, $b]= [$b, $a]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test, Values([[null, true], [false, false], ['Test', 'Test']])] + public function null_coalesce_assigns_true_if_null($value, $expected) { + $r= $this->run('class %T { + public function run($arg) { + $arg??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r); + } + + #[Test, Values([[[], true], [[null], true], [[false], false], [['Test'], 'Test']])] + public function null_coalesce_fills_array_if_non_existant_or_null($value, $expected) { + $r= $this->run('class %T { + public function run($arg) { + $arg[0]??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r[0]); + } +} \ No newline at end of file From 13ac79247e7e272faaa9617dd90a079df81dd9e0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Mar 2024 21:59:08 +0100 Subject: [PATCH 835/926] Add tests for bitwise operators --- .../lang/ast/unittest/emit/OperatorTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index 0e1833b0..4ce4ec5e 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -17,6 +17,19 @@ public function run() { Assert::equals($expected, $r); } + #[Test, Values([['|=', 0x0003], ['&=', 0x0002], ['^=', 0x0001]])] + public function assignment_and_bitwise($op, $expected) { + $r= $this->run('class %T { + public function run() { + $a= 0x0003; + $a'.$op.' 0x0002; + return $a; + } + }'); + + Assert::equals($expected, $r); + } + #[Test] public function destructuring() { $r= $this->run('class %T { From 36eb6f10d88de26c2ef9cfc6d956a815708d4023 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Mar 2024 22:06:23 +0100 Subject: [PATCH 836/926] Add tests for various other operators See https://www.php.net/manual/en/language.operators.assignment.php --- .../ast/unittest/emit/OperatorTest.class.php | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index 4ce4ec5e..cc4447cd 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -4,7 +4,7 @@ class OperatorTest extends EmittingTest { - #[Test, Values([['+=', 3], ['-=', -1], ['*=', 2], ['/=', 0.5]])] + #[Test, Values([['+=', 3], ['-=', -1], ['*=', 2], ['/=', 0.5], ['**=', 1]])] public function assignment_and_math($op, $expected) { $r= $this->run('class %T { public function run() { @@ -17,7 +17,7 @@ public function run() { Assert::equals($expected, $r); } - #[Test, Values([['|=', 0x0003], ['&=', 0x0002], ['^=', 0x0001]])] + #[Test, Values([['|=', 0x0003], ['&=', 0x0002], ['^=', 0x0001], ['>>=', 0x0000], ['<<=', 0x000C]])] public function assignment_and_bitwise($op, $expected) { $r= $this->run('class %T { public function run() { @@ -30,6 +30,46 @@ public function run() { Assert::equals($expected, $r); } + #[Test] + public function concatenation() { + $r= $this->run('class %T { + public function run() { + $a= "A.."; + $a.= "B"; + return $a; + } + }'); + + Assert::equals('A..B', $r); + } + + #[Test, Values([['$a++', 2, 1], ['++$a', 2, 2], ['$a--', 0, 1], ['--$a', 0, 0]])] + public function inc_dec($op, $a, $b) { + $r= $this->run('class %T { + public function run() { + $a= 1; + $b= '.$op.'; + return [$a, $b]; + } + }'); + + Assert::equals([$a, $b], $r); + } + + #[Test] + public function references() { + $r= $this->run('class %T { + public function run() { + $a= 3; + $ptr= &$a; + $a++; + return $ptr; + } + }'); + + Assert::equals(4, $r); + } + #[Test] public function destructuring() { $r= $this->run('class %T { From bdf5a5425527d72d67e16183c0694e6ae6d9af8a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 24 Mar 2024 22:10:24 +0100 Subject: [PATCH 837/926] Add test for `%=` See https://github.com/xp-framework/ast/releases/tag/v11.0.1 --- src/test/php/lang/ast/unittest/emit/OperatorTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php index cc4447cd..061a0d0e 100755 --- a/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/OperatorTest.class.php @@ -4,7 +4,7 @@ class OperatorTest extends EmittingTest { - #[Test, Values([['+=', 3], ['-=', -1], ['*=', 2], ['/=', 0.5], ['**=', 1]])] + #[Test, Values([['+=', 3], ['-=', -1], ['*=', 2], ['/=', 0.5], ['**=', 1], ['%=', 1]])] public function assignment_and_math($op, $expected) { $r= $this->run('class %T { public function run() { From 1a5e7e1bdd7ca9bf64bf396096c22ed08c1ae5b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 27 Mar 2024 13:32:06 +0100 Subject: [PATCH 838/926] Add support for pipelines with `|>` and `?|>` --- src/main/php/lang/ast/emit/PHP.class.php | 40 ++++++++++ .../ast/unittest/emit/PipelinesTest.class.php | 75 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index ac3d93a4..d86cdb7c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -7,6 +7,8 @@ ArrayLiteral, BinaryExpression, Block, + CallableExpression, + CallableNewExpression, Comment, Expression, InstanceExpression, @@ -1129,6 +1131,44 @@ protected function emitNullsafeInstance($result, $instance) { $this->emitOne($result, $instance->member); } + protected function emitPipeTarget($result, $pipe, $argument) { + + // $expr |> new T(...) => new T($expr) + if ($pipe->target instanceof CallableNewExpression) { + $pipe->target->type->arguments= [$argument]; + $this->emitOne($result, $pipe->target->type); + $pipe->target->type->arguments= null; + return; + } + + // $expr |> strtoupper(...) => strtoupper($expr) + // $expr |> fn($x) => $x * 2 => (fn($x) => $x * 2)($expr) + if ($pipe->target instanceof CallableExpression) { + $this->emitOne($result, $pipe->target->expression); + } else { + $result->out->write('('); + $this->emitOne($result, $pipe->target); + $result->out->write(')'); + } + + $result->out->write('('); + $this->emitOne($result, $argument); + $result->out->write(')'); + } + + protected function emitPipe($result, $pipe) { + $this->emitPipeTarget($result, $pipe, $pipe->expression); + } + + protected function emitNullsafePipe($result, $pipe) { + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(')?null:'); + + $this->emitPipeTarget($result, $pipe, new Variable(substr($t, 1))); + } + protected function emitUnpack($result, $unpack) { $result->out->write('...'); $this->emitOne($result, $unpack->expression); diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php new file mode 100755 index 00000000..7fa46a9f --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -0,0 +1,75 @@ +run('class %T { + public function run() { + return "test" |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_new() { + $r= $this->run('class %T { + public function run() { + return "2024-03-27" |> new \util\Date(...); + } + }'); + + Assert::equals('2024-03-27', $r->toString('Y-m-d')); + } + + #[Test] + public function pipe_to_callable_anonymous_new() { + $r= $this->run('class %T { + public function run() { + return "2024-03-27" |> new class(...) { + public function __construct(public string $value) { } + }; + } + }'); + + Assert::equals('2024-03-27', $r->value); + } + + #[Test] + public function pipe_to_closure() { + $r= $this->run('class %T { + public function run() { + return "test" |> fn($x) => $x.": OK"; + } + }'); + + Assert::equals('test: OK', $r); + } + + #[Test] + public function pipe_chain() { + $r= $this->run('class %T { + public function run() { + return " test " |> trim(...) |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test, Values([[null, null], ['test', 'TEST'], [' test ', 'TEST']])] + public function nullsafe_pipe($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return $arg ?|> trim(...) ?|> strtoupper(...); + } + }', $input); + + Assert::equals($expected, $r); + } +} \ No newline at end of file From 196dfb9d055d2262e143045564d6aea1f83f9969 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 27 Mar 2024 13:37:47 +0100 Subject: [PATCH 839/926] QA: Remove unused imports --- src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index 7fa46a9f..cff7cd90 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -1,7 +1,6 @@ Date: Wed, 27 Mar 2024 13:42:10 +0100 Subject: [PATCH 840/926] Add test ensuring the expression is only invoked once --- .../lang/ast/unittest/emit/PipelinesTest.class.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index cff7cd90..0b10220f 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -61,8 +61,19 @@ public function run() { Assert::equals('TEST', $r); } - #[Test, Values([[null, null], ['test', 'TEST'], [' test ', 'TEST']])] + #[Test, Values([[['test'], 'TEST'], [[], null]])] public function nullsafe_pipe($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return array_shift($arg) ?|> strtoupper(...); + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[null, null], ['test', 'TEST'], [' test ', 'TEST']])] + public function nullsafe_chain($input, $expected) { $r= $this->run('class %T { public function run($arg) { return $arg ?|> trim(...) ?|> strtoupper(...); From 725e453642f22c9d744cf430fbf9da969c0ecc1c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 27 Mar 2024 14:13:16 +0100 Subject: [PATCH 841/926] Add tests for a variety of callables --- .../ast/unittest/emit/PipelinesTest.class.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index 0b10220f..8e5dbeec 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -15,6 +15,53 @@ public function run() { Assert::equals('TEST', $r); } + #[Test] + public function pipe_to_variable() { + $r= $this->run('class %T { + public function run() { + $f= strtoupper(...); + return "test" |> $f; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_string() { + $r= $this->run('class %T { + public function run() { + return "test" |> "strtoupper"; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_array() { + $r= $this->run('class %T { + public function toUpper($x) { return strtoupper($x); } + + public function run() { + return "test" |> [$this, "toUpper"]; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_without_all_args() { + $r= $this->run('class %T { + public function run() { + return "A&B" |> htmlspecialchars(...); + } + }'); + + Assert::equals('A&B', $r); + } + #[Test] public function pipe_to_callable_new() { $r= $this->run('class %T { From 00d9658885f6b2e645eea1ca8571aa134e59c1c4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 27 Mar 2024 15:09:38 +0100 Subject: [PATCH 842/926] Fix xp-framework/ast dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 303b7c68..af9cf190 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": "^11.0 | ^10.1", + "xp-framework/ast": "dev-feature/pipelines as 11.1.0", "php" : ">=7.4.0" }, "require-dev" : { From f59cad20affb179036ef4fce7bec18e302ed7004 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 28 Mar 2024 21:53:28 +0100 Subject: [PATCH 843/926] Fix without_namespace() test --- src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php index 7fe7dcc7..a4dc258f 100755 --- a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php @@ -7,7 +7,7 @@ class NamespacesTest extends EmittingTest { #[Test] public function without_namespace() { - Assert::null($this->declare('class %T { }')->package()); + Assert::false(strpos($this->declare('class %T { }')->name(), '.')); } #[Test] From b74ec9a83d4c1755abc9af9bfe13def7a636c717 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 28 Mar 2024 21:55:37 +0100 Subject: [PATCH 844/926] Use new reflection API --- .../unittest/loader/CompilingClassLoaderTest.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 2c04bb04..604d6fcf 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -2,7 +2,7 @@ use io\{File, Files, Folder}; use lang\ast\CompilingClassLoader; -use lang\{ClassFormatException, ClassLoader, ClassNotFoundException, ElementNotFoundException, Environment}; +use lang\{ClassFormatException, ClassLoader, ClassNotFoundException, ElementNotFoundException, Environment, Reflection}; use test\{Action, Assert, Expect, Test, Values}; class CompilingClassLoaderTest { @@ -108,11 +108,11 @@ public function load_dependencies() { 'Feature' => 'compile($source, fn($loader, $types) => $loader->loadClass($types['Child'])); - $n= fn($class) => $class->getSimpleName(); + $t= Reflection::type($this->compile($source, fn($loader, $types) => $loader->loadClass($types['Child']))); + $n= fn($class) => $class->declaredName(); Assert::equals( ['Child', 'Base', ['Impl'], ['Feature']], - [$n($c), $n($c->getParentClass()), array_map($n, $c->getInterfaces()), array_map($n, $c->getTraits())] + [$n($t), $n($t->parent()), array_map($n, $t->interfaces()), array_map($n, $t->traits())] ); } From 39f49298b4d3d72175364301f422324ae7d2a70f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:03:42 +0800 Subject: [PATCH 845/926] 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 846/926] 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 847/926] 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 848/926] 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 849/926] 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 850/926] 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 851/926] 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 852/926] 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" : { From b4834c95474733acdd2c39b60fed0aea1897c8cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jun 2024 10:56:48 +0200 Subject: [PATCH 853/926] Release 9.1.0 --- ChangeLog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a1b764d1..97020c13 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,14 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.1.0 / 2024-06-15 + +* Merged PR #166: Implement property hooks via virtual properties, see + https://wiki.php.net/rfc/property-hooks. Includes support for native + implementation, which is yet to be merged (php/php-src#13455). Thus, + this still might be a moving target in some regards! + (@thekid) + ## 9.0.0 / 2024-03-23 * Merged PR #179: XP 12 compatibility, dropping PHP 7.0 - 7.3 support! From 48f60ae3a0779359e18103f8d2a341fe471d0843 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jun 2024 11:14:20 +0200 Subject: [PATCH 854/926] Add PHP 8.4 feature to example [skip ci] --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 897ab5b2..ad72710c 100755 --- a/README.md +++ b/README.md @@ -16,24 +16,25 @@ After adding the compiler to your project via `composer require xp-framework/com Example ------- -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. +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. ```php $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; $author= Reflection::type(self::class)->annotation(Author::class)->argument(0); - Console::writeLine($greet($args[0] ?? 'World', from: $author)); + Console::writeLine(new Date()->toString(), ': ', $greet($args[0] ?? 'World', from: $author)); } } ``` From a3c10dffe48a36df9e9558b46308b57453f86c6a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 17 Jul 2024 19:55:34 +0200 Subject: [PATCH 855/926] Use SVG badge [skip ci] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad72710c..c3f90b34 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ XP Compiler [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) [![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/) [![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/) -[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) +[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.svg)](https://packagist.org/packages/xp-framework/compiler) Compiles future PHP to today's PHP. From 63d7f83e68a9c6690df47e458da9a34ef3232fda Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 11 Aug 2024 23:51:59 +0200 Subject: [PATCH 856/926] Add test for grouped imports with alias See https://github.com/xp-framework/ast/releases/tag/v11.2.1 --- .../php/lang/ast/unittest/emit/ImportTest.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index e44269c2..81c4f5e4 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -33,6 +33,17 @@ public function run() { return D::class; } )); } + #[Test] + public function import_type_as_alias_in_group() { + Assert::equals(Date::class, $this->run(' + use util\{Objects, Date as D}; + + class %T { + public function run() { return D::class; } + }' + )); + } + #[Test] public function import_const() { Assert::equals('imported', $this->run(' From dbc916cfac8f9281d07696c816e96091a819b6bf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 12:57:14 +0200 Subject: [PATCH 857/926] Add forward compatibility for Parameter::$promote becoming an array --- src/main/php/lang/ast/emit/PHP.class.php | 6 +++++- src/main/php/lang/ast/emit/ReadonlyClasses.class.php | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d83c5cc7..b59100d9 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -718,7 +718,11 @@ protected function emitMethod($result, $method) { } foreach ($promoted as $param) { - $this->emitProperty($result, new Property(explode(' ', $param->promote), $param->name, $param->type)); + $this->emitProperty($result, new Property( + is_array($param->promote) ? $param->promote : explode(' ', $param->promote), + $param->name, + $param->type + )); } $result->locals= $locals; diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index 715c9b84..e24d11ab 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -21,7 +21,13 @@ protected function emitClass($result, $class) { $member->modifiers[]= 'readonly'; } else if ($member->is('method')) { foreach ($member->signature->parameters as $param) { - $param->promote && $param->promote.= ' readonly'; + if (null === $param->promote) { + // NOOP + } else if (is_array($param->promote)) { + $param->promote[]= 'readonly'; + } else if (is_string($param->promote)) { + $param->promote.= ' readonly'; + } } } } From 401ef99b005e84a89507b6a884cbb66f40118345 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 13:55:18 +0200 Subject: [PATCH 858/926] Add emitting support for asymmetric visibility --- .../ast/emit/AsymmetricVisibility.class.php | 37 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP81.class.php | 4 +- src/main/php/lang/ast/emit/PHP82.class.php | 4 +- src/main/php/lang/ast/emit/PHP83.class.php | 2 +- .../lang/ast/emit/RewriteProperties.class.php | 7 +++- .../emit/AsymmetricVisibilityTest.class.php | 28 ++++++++++++++ 6 files changed, 75 insertions(+), 7 deletions(-) create mode 100755 src/main/php/lang/ast/emit/AsymmetricVisibility.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php new file mode 100755 index 00000000..f83f856b --- /dev/null +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -0,0 +1,37 @@ +name}'"); + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); + + if (in_array('private(set)', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' + ); + } + + $scope= $result->codegen->scope[0]; + $scope->virtual[$property->name]= [ + new ReturnStatement($virtual), + new Block([new Code($check), new Assignment($virtual, '=', new Variable('value'))]), + ]; + if (isset($property->expression)) { + $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 70d51ba5..de5db7dc 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -23,9 +23,9 @@ class PHP81 extends PHP { RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, + RewriteProperties, ReadonlyClasses, - OmitConstantTypes, - PropertyHooks + OmitConstantTypes ; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 0b568d29..8d0c1b96 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -23,9 +23,9 @@ class PHP82 extends PHP { RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, + RewriteProperties, ReadonlyClasses, - OmitConstantTypes, - PropertyHooks + OmitConstantTypes ; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 9285e65d..7ae725f7 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, PropertyHooks; + use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 29a0b4d6..e4cb7bb9 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -1,15 +1,18 @@ hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('readonly', $property->modifiers)) { + } else if (in_array('private(set)', $property->modifiers)) { + return $this->emitAsymmetricVisibility($result, $property); + } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); } parent::emitProperty($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php new file mode 100755 index 00000000..ca97d0d1 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -0,0 +1,28 @@ +declare('class %T { + public private(set) $fixture= "Test"; + }'); + Assert::equals('Test', $t->newInstance()->fixture); + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] + public function writing() { + $t= $this->declare('class %T { + public private(set) $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } +} \ No newline at end of file From a6be1f6cb0f16647b910a4585464fe21541a1767 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 13:59:52 +0200 Subject: [PATCH 859/926] Require feature branch See xp-framework/ast#54 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ed40d3f1..1d63b8cc 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": "^11.1", + "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, "require-dev" : { From 7172e6ee52605965ad941c2ff42412587dd17c92 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:02:58 +0200 Subject: [PATCH 860/926] Add support for protected(set) --- .../php/lang/ast/emit/AsymmetricVisibility.class.php | 6 ++++++ src/main/php/lang/ast/emit/RewriteProperties.class.php | 2 +- .../unittest/emit/AsymmetricVisibilityTest.class.php | 10 +++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index f83f856b..d7e85488 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -23,6 +23,12 @@ protected function emitProperty($result, $property) { 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' ); + } else if (in_array('protected(set)', $property->modifiers)) { + $check= ( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' + ); } $scope= $result->codegen->scope[0]; diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index e4cb7bb9..6715fd20 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,7 +10,7 @@ trait RewriteProperties { protected function emitProperty($result, $property) { if ($property->hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('private(set)', $property->modifiers)) { + } else if (in_array('private(set)', $property->modifiers) || in_array('protected(set)', $property->modifiers)) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index ca97d0d1..e02bc9ca 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -19,10 +19,18 @@ public function reading() { } #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] - public function writing() { + public function writing_private() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property T.+::fixture/')] + public function writing_protected() { + $t= $this->declare('class %T { + public protected(set) $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } } \ No newline at end of file From 6760f345f911af0f95a619d9c0d8fa50b68d36a9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:41:20 +0200 Subject: [PATCH 861/926] Make errors consistent with PHP implementation See https://github.com/php/php-src/pull/15063/files#diff-5a3c9d6fc60178fb68e7c0fde07798c26bb71277f57f265043c1faf329b6ba25R43 --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 4 ++-- .../lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index d7e85488..48d6096a 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -21,13 +21,13 @@ protected function emitProperty($result, $property) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' + 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' ); } else if (in_array('protected(set)', $property->modifiers)) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' + 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' ); } diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index e02bc9ca..adf42921 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -18,7 +18,7 @@ public function reading() { Assert::equals('Test', $t->newInstance()->fixture); } - #[Test, Expect(class: Error::class, message: '/Cannot access private property T.+::fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; @@ -26,7 +26,7 @@ public function writing_private() { $t->newInstance()->fixture= 'Changed'; } - #[Test, Expect(class: Error::class, message: '/Cannot access protected property T.+::fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot modify protected\(set\) property T.+::\$fixture/')] public function writing_protected() { $t= $this->declare('class %T { public protected(set) $fixture= "Test"; From 865d2e982b733da4a8aa1fc11e602e5be280bce5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:44:42 +0200 Subject: [PATCH 862/926] Test writing from within private context --- .../emit/AsymmetricVisibilityTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index adf42921..3d4f8bec 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -18,6 +18,19 @@ public function reading() { Assert::equals('Test', $t->newInstance()->fixture); } + #[Test] + public function writing() { + $t= $this->declare('class %T { + public private(set) $fixture= "Test"; + + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { From f281452e8f4f3d4f4363ea557c94b169736ffaae Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 14:48:02 +0200 Subject: [PATCH 863/926] Test promoted constructor parameters --- .../ast/unittest/emit/AsymmetricVisibilityTest.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 3d4f8bec..3c5781c7 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -46,4 +46,12 @@ public function writing_protected() { }'); $t->newInstance()->fixture= 'Changed'; } + + #[Test] + public function promoted_constructor_parameter() { + $t= $this->declare('class %T { + public function __construct(public private(set) $fixture) { } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } } \ No newline at end of file From 72cde2318956296fc49aaf1474ff0d9f1ee50386 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:05:46 +0200 Subject: [PATCH 864/926] Allow explicit `public(set)` --- .../lang/ast/emit/AsymmetricVisibility.class.php | 12 +++++++----- .../php/lang/ast/emit/RewriteProperties.class.php | 2 +- .../emit/AsymmetricVisibilityTest.class.php | 13 ++++++++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 48d6096a..66dbfaf2 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -18,23 +18,25 @@ protected function emitProperty($result, $property) { $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); if (in_array('private(set)', $property->modifiers)) { - $check= ( + $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - ); + )]; } else if (in_array('protected(set)', $property->modifiers)) { - $check= ( + $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - ); + )]; + } else { + $check= []; } $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), - new Block([new Code($check), new Assignment($virtual, '=', new Variable('value'))]), + new Block([...$check, new Assignment($virtual, '=', new Variable('value'))]), ]; if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 6715fd20..094a8298 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,7 +10,7 @@ trait RewriteProperties { protected function emitProperty($result, $property) { if ($property->hooks) { return $this->emitPropertyHooks($result, $property); - } else if (in_array('private(set)', $property->modifiers) || in_array('protected(set)', $property->modifiers)) { + } else if (array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 3c5781c7..acfe9b73 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -19,7 +19,7 @@ public function reading() { } #[Test] - public function writing() { + public function writing_from_self_scope() { $t= $this->declare('class %T { public private(set) $fixture= "Test"; @@ -31,6 +31,17 @@ public function rename($name) { Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); } + #[Test] + public function writing_explicitely_public_set() { + $t= $this->declare('class %T { + public public(set) $fixture= "Test"; + }'); + + $instance= $t->newInstance(); + $instance->fixture= 'Changed'; + Assert::equals('Changed', $instance->fixture); + } + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { From ee835aaca8a532541503a3eacdf408b4d7e8fb24 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:07:47 +0200 Subject: [PATCH 865/926] QA: Code locality --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 66dbfaf2..346359e4 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -14,9 +14,6 @@ trait AsymmetricVisibility { protected function emitProperty($result, $property) { - $literal= new Literal("'{$property->name}'"); - $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); - if (in_array('private(set)', $property->modifiers)) { $check= [new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. @@ -33,6 +30,11 @@ protected function emitProperty($result, $property) { $check= []; } + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), From b1f428cdcbb95e2cbc5936301f7de1fba397e310 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 15:27:32 +0200 Subject: [PATCH 866/926] Extract visibility checks into trait --- .../ast/emit/AsymmetricVisibility.class.php | 14 +---- .../php/lang/ast/emit/PropertyHooks.class.php | 16 +----- .../ast/emit/ReadonlyProperties.class.php | 55 +++++++++++-------- .../lang/ast/emit/VisibilityChecks.class.php | 22 ++++++++ 4 files changed, 61 insertions(+), 46 deletions(-) create mode 100755 src/main/php/lang/ast/emit/VisibilityChecks.class.php diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 346359e4..514516f1 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -1,6 +1,5 @@ modifiers)) { - $check= [new Code( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot modify private(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - )]; + $check= [$this->private($property->name, 'modify private(set)')]; } else if (in_array('protected(set)', $property->modifiers)) { - $check= [new Code( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot modify protected(set) property ".__CLASS__."::\$".$name." from ".($scope ? "scope ".$scope : "global scope"));' - )]; + $check= [$this->protected($property->name, 'modify protected(set)')]; } else { $check= []; } diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 621edbfb..1859c521 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -1,7 +1,6 @@ private('$name', 'access private'), ...$nodes]); } else if ($modifiers & MODIFIER_PROTECTED) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' - ); + return new Block([$this->protected('$name', 'access protected'), ...$nodes]); } else if (1 === sizeof($nodes)) { return $nodes[0]; } else { return new Block($nodes); } - - return new Block([new Code($check), ...$nodes]); } protected function emitEmulatedHooks($result, $property) { diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 3356b426..69b75de0 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -1,5 +1,15 @@ modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");' - ); - } else if (in_array('protected', $property->modifiers)) { - $check= ( - '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. - 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");' - ); + if ($modifiers & MODIFIER_PRIVATE) { + $check= $this->private($property->name, 'access private'); + } else if ($modifiers & MODIFIER_PROTECTED) { + $check= $this->protected($property->name, 'access protected'); } else { - $check= ''; + $check= null; } + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + // Create virtual property implementing the readonly semantics $scope->virtual[$property->name]= [ - new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)), - new Code(sprintf( - ($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'). - 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. - 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. - 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. - '$this->__virtual["%1$s"]= [$value];', - $property->name - )), + $check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual), + new Block([ + $check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'), + new Code(sprintf( + 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. + '$this->__virtual["%1$s"]= [$value];', + $property->name + )), + new Assignment($virtual, '=', new Variable('value')) + ]), ]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/VisibilityChecks.class.php b/src/main/php/lang/ast/emit/VisibilityChecks.class.php new file mode 100755 index 00000000..3b702b54 --- /dev/null +++ b/src/main/php/lang/ast/emit/VisibilityChecks.class.php @@ -0,0 +1,22 @@ + Date: Sat, 24 Aug 2024 16:01:21 +0200 Subject: [PATCH 867/926] QA: API docs --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 514516f1..f4f07d23 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -10,6 +10,12 @@ Variable }; +/** + * Asymmetric Visibility + * + * @see https://wiki.php.net/rfc/asymmetric-visibility-v2 + * @test lang.ast.unittest.emit.AsymmetricVisibilityTest + */ trait AsymmetricVisibility { use VisibilityChecks; From 59ed2c1f48f53d46d2ad0ec9078185732e7c5752 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 16:12:20 +0200 Subject: [PATCH 868/926] Verify setting protected(set) from inherited scope --- .../emit/AsymmetricVisibilityTest.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index acfe9b73..5e59d59d 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -28,6 +28,22 @@ public function rename($name) { return $this; } }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + + #[Test] + public function writing_from_inherited_scope() { + $parent= $this->declare('class %T { public protected(set) $fixture= "Test"; }'); + $t= $this->declare('class %T extends '.$parent->literal().' { + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); } From f553374ba350620f5b2eddfa292961d65ec4fb1b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Aug 2024 16:31:07 +0200 Subject: [PATCH 869/926] Support readonly --- .../lang/ast/emit/AsymmetricVisibility.class.php | 14 +++++++++----- .../php/lang/ast/emit/ReadonlyProperties.class.php | 2 +- .../php/lang/ast/emit/VisibilityChecks.class.php | 4 ++++ .../emit/AsymmetricVisibilityTest.class.php | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index f4f07d23..3fced9e3 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -20,12 +20,16 @@ trait AsymmetricVisibility { use VisibilityChecks; protected function emitProperty($result, $property) { + $checks= []; if (in_array('private(set)', $property->modifiers)) { - $check= [$this->private($property->name, 'modify private(set)')]; + $checks[]= $this->private($property->name, 'modify private(set)'); } else if (in_array('protected(set)', $property->modifiers)) { - $check= [$this->protected($property->name, 'modify protected(set)')]; - } else { - $check= []; + $checks[]= $this->protected($property->name, 'modify protected(set)'); + } + + // The readonly flag is really two flags in one: write-once and restricted(set) + if (in_array('readonly', $property->modifiers)) { + $checks[]= $this->initonce($property->name); } $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( @@ -36,7 +40,7 @@ protected function emitProperty($result, $property) { $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), - new Block([...$check, new Assignment($virtual, '=', new Variable('value'))]), + new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]), ]; if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 69b75de0..82530fec 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -66,8 +66,8 @@ protected function emitProperty($result, $property) { $check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual), new Block([ $check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'), + $this->initonce($property->name), new Code(sprintf( - 'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'. 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. '$this->__virtual["%1$s"]= [$value];', diff --git a/src/main/php/lang/ast/emit/VisibilityChecks.class.php b/src/main/php/lang/ast/emit/VisibilityChecks.class.php index 3b702b54..45672731 100755 --- a/src/main/php/lang/ast/emit/VisibilityChecks.class.php +++ b/src/main/php/lang/ast/emit/VisibilityChecks.class.php @@ -4,6 +4,10 @@ trait VisibilityChecks { + private function initonce($name) { + return new Code('if (isset($this->__virtual["'.$name.'"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::\$'.$name.'");'); + } + private function private($name, $access) { return new Code( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 5e59d59d..75dbe770 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -81,4 +81,18 @@ public function __construct(public private(set) $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); } + + #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] + public function readonly() { + $t= $this->declare('class %T { + + // public-read, protected-write, write-once property + public protected(set) readonly string $fixture= "Test"; + + public function rename() { + $this->fixture= "Changed"; // Will always error + } + }'); + $t->newInstance()->rename(); + } } \ No newline at end of file From 7fba42769f415029be686e5d718d5ca0d9e04459 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 09:15:26 +0200 Subject: [PATCH 870/926] Add reflection Requires https://github.com/xp-framework/reflection/pull/43 --- composer.json | 2 +- .../ast/emit/AsymmetricVisibility.class.php | 33 ++++++++++++++++--- .../emit/AsymmetricVisibilityTest.class.php | 14 +++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 1d63b8cc..5dcb4c8d 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", - "xp-framework/reflection": "^3.0 | ^2.13", + "xp-framework/reflection": "dev-feature/asymmetric-visibility as 3.2.0", "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 3fced9e3..60883f82 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -20,10 +20,37 @@ trait AsymmetricVisibility { use VisibilityChecks; protected function emitProperty($result, $property) { + static $lookup= [ + 'public' => MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => MODIFIER_READONLY, + 'private(set)' => 0x0400, + 'protected(set)' => 0x0800, + 'public(set)' => 0x1000, + ]; + + // 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] + ]; + $checks= []; - if (in_array('private(set)', $property->modifiers)) { + if ($modifiers & 0x0400) { $checks[]= $this->private($property->name, 'modify private(set)'); - } else if (in_array('protected(set)', $property->modifiers)) { + } else if ($modifiers & 0x0800) { $checks[]= $this->protected($property->name, 'modify protected(set)'); } @@ -36,8 +63,6 @@ protected function emitProperty($result, $property) { new Literal('__virtual'), new Literal("'{$property->name}'")) ); - - $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [ new ReturnStatement($virtual), new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]), diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 75dbe770..d822373c 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -1,7 +1,7 @@ newInstance()->rename(); } + + #[Test, Values(['private', 'protected', 'public'])] + public function reflection($modifier) { + $t= $this->declare('class %T { + public '.$modifier.'(set) string $fixture= "Test"; + }'); + + Assert::equals( + 'public '.$modifier.'(set) string $fixture', + $t->property('fixture')->toString() + ); + } } \ No newline at end of file From c1fb7dd2a01c759d7ec672bb9cad3b0ed66d76d6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:08:24 +0200 Subject: [PATCH 871/926] Readonly implies `protected(set)` --- .../php/lang/ast/unittest/emit/ReadonlyTest.class.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 9c8d1db5..a998a7dd 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -27,7 +27,7 @@ public function class_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -39,7 +39,7 @@ public function property_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -51,7 +51,7 @@ public function __construct(public string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); @@ -64,7 +64,7 @@ public function __construct(public readonly string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); From 241655713af7bcae0d2a0c6048a1d593a8697367 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:10:07 +0200 Subject: [PATCH 872/926] Adjust constants after integration-testing with PHP compiled from PR --- src/main/php/lang/ast/emit/AsymmetricVisibility.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 60883f82..7692bf84 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -28,9 +28,9 @@ protected function emitProperty($result, $property) { 'final' => MODIFIER_FINAL, 'abstract' => MODIFIER_ABSTRACT, 'readonly' => MODIFIER_READONLY, - 'private(set)' => 0x0400, + 'public(set)' => 0x0400, 'protected(set)' => 0x0800, - 'public(set)' => 0x1000, + 'private(set)' => 0x1000, ]; // Emit XP meta information for the reflection API @@ -48,7 +48,7 @@ protected function emitProperty($result, $property) { ]; $checks= []; - if ($modifiers & 0x0400) { + if ($modifiers & 0x1000) { $checks[]= $this->private($property->name, 'modify private(set)'); } else if ($modifiers & 0x0800) { $checks[]= $this->protected($property->name, 'modify protected(set)'); From 7c9cee8289442acfb9dc90f84f0f9416388c950b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 11:35:48 +0200 Subject: [PATCH 873/926] Adjust expected error message to work for PHP compiled from PR branch See https://github.com/php/php-src/pull/15063/files#diff-4ffd79a0cf90a14777c4fe03429e25ec414ca63e8a4535319699bc119f52add0R58 --- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index a998a7dd..c65e4a66 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -151,7 +151,7 @@ public function can_be_assigned_via_reflection() { Assert::equals('Test', $i->fixture); } - #[Test, Expect(class: Error::class, message: '/Cannot initialize readonly property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot (initialize readonly|modify protected\(set\)) property .+fixture/')] public function cannot_initialize_from_outside() { $t= $this->declare('class %T { public readonly string $fixture; From 501c3d512058fb064ec5e6d3913f7ed4ec90822f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 12:32:21 +0200 Subject: [PATCH 874/926] Remove test for `public public(set)`, where the set hook is erased --- .../lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index d822373c..4b404a9d 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -96,7 +96,7 @@ public function rename() { $t->newInstance()->rename(); } - #[Test, Values(['private', 'protected', 'public'])] + #[Test, Values(['private', 'protected'])] public function reflection($modifier) { $t= $this->declare('class %T { public '.$modifier.'(set) string $fixture= "Test"; From d0e2732596637bda033480e968bc8d13f679c921 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 12:51:01 +0200 Subject: [PATCH 875/926] Fold declarations like `[visibility] [visibility](set)` to just the visibility itself --- .../ast/emit/AsymmetricVisibility.class.php | 23 ++++++++++++------- .../emit/AsymmetricVisibilityTest.class.php | 12 ++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 7692bf84..8daac83a 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -33,12 +33,26 @@ protected function emitProperty($result, $property) { 'private(set)' => 0x1000, ]; - // Emit XP meta information for the reflection API $scope= $result->codegen->scope[0]; $modifiers= 0; foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } + + // Declare checks for private(set) and protected(set), folding declarations + // like `[visibility] [visibility](set)` to just the visibility itself. + if ($modifiers & 0x0400) { + $checks= []; + $modifiers&= ~0x0400; + } else if ($modifiers & 0x0800) { + $checks= [$this->protected($property->name, 'modify protected(set)')]; + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0800; + } else if ($modifiers & 0x1000) { + $checks= [$this->private($property->name, 'modify private(set)')]; + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x1000; + } + + // Emit XP meta information for the reflection API $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, @@ -47,13 +61,6 @@ protected function emitProperty($result, $property) { DETAIL_ARGUMENTS => [$modifiers] ]; - $checks= []; - if ($modifiers & 0x1000) { - $checks[]= $this->private($property->name, 'modify private(set)'); - } else if ($modifiers & 0x0800) { - $checks[]= $this->protected($property->name, 'modify protected(set)'); - } - // The readonly flag is really two flags in one: write-once and restricted(set) if (in_array('readonly', $property->modifiers)) { $checks[]= $this->initonce($property->name); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 4b404a9d..ca67f06c 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -107,4 +107,16 @@ public function reflection($modifier) { $t->property('fixture')->toString() ); } + + #[Test, Values(['private', 'protected', 'public'])] + public function same_modifier_for_get_and_set($modifier) { + $t= $this->declare('class %T { + '.$modifier.' '.$modifier.'(set) string $fixture= "Test"; + }'); + + Assert::equals( + $modifier.' string $fixture', + $t->property('fixture')->toString() + ); + } } \ No newline at end of file From 28403bd4a41d3030f59d806355c979f0f22fdfa6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 13:50:57 +0200 Subject: [PATCH 876/926] Add support for emitting properties with asymmetric visibility natively --- .../lang/ast/emit/RewriteProperties.class.php | 8 +++++++- .../emit/AsymmetricVisibilityTest.class.php | 20 +++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index 094a8298..d0a50e81 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -1,5 +1,7 @@ hooks) { return $this->emitPropertyHooks($result, $property); - } else if (array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])) { + } else if ( + !($asymmetric ?? $asymmetric= method_exists(ReflectionProperty::class, 'isPrivateSet')) && + array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) + ) { return $this->emitAsymmetricVisibility($result, $property); } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { return $this->emitReadonlyProperties($result, $property); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index ca67f06c..1c327efe 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -13,7 +13,7 @@ class AsymmetricVisibilityTest extends EmittingTest { #[Test] public function reading() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; }'); Assert::equals('Test', $t->newInstance()->fixture); } @@ -21,7 +21,7 @@ public function reading() { #[Test] public function writing_from_self_scope() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; public function rename($name) { $this->fixture= $name; @@ -35,7 +35,7 @@ public function rename($name) { #[Test] public function writing_from_inherited_scope() { - $parent= $this->declare('class %T { public protected(set) $fixture= "Test"; }'); + $parent= $this->declare('class %T { public protected(set) string $fixture= "Test"; }'); $t= $this->declare('class %T extends '.$parent->literal().' { public function rename($name) { $this->fixture= $name; @@ -50,7 +50,7 @@ public function rename($name) { #[Test] public function writing_explicitely_public_set() { $t= $this->declare('class %T { - public public(set) $fixture= "Test"; + public public(set) string $fixture= "Test"; }'); $instance= $t->newInstance(); @@ -61,7 +61,7 @@ public function writing_explicitely_public_set() { #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] public function writing_private() { $t= $this->declare('class %T { - public private(set) $fixture= "Test"; + public private(set) string $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } @@ -69,7 +69,7 @@ public function writing_private() { #[Test, Expect(class: Error::class, message: '/Cannot modify protected\(set\) property T.+::\$fixture/')] public function writing_protected() { $t= $this->declare('class %T { - public protected(set) $fixture= "Test"; + public protected(set) string $fixture= "Test"; }'); $t->newInstance()->fixture= 'Changed'; } @@ -77,7 +77,7 @@ public function writing_protected() { #[Test] public function promoted_constructor_parameter() { $t= $this->declare('class %T { - public function __construct(public private(set) $fixture) { } + public function __construct(public private(set) string $fixture) { } }'); Assert::equals('Test', $t->newInstance('Test')->fixture); } @@ -87,7 +87,11 @@ public function readonly() { $t= $this->declare('class %T { // public-read, protected-write, write-once property - public protected(set) readonly string $fixture= "Test"; + public protected(set) readonly string $fixture; + + public function __construct() { + $this->fixture= "Test"; + } public function rename() { $this->fixture= "Changed"; // Will always error From 4a35832958195887b0a86f37f644aca7c9bae9a3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 25 Aug 2024 21:32:47 +0200 Subject: [PATCH 877/926] Test against target version, not runtime version --- src/main/php/lang/ast/emit/PHP74.class.php | 2 ++ src/main/php/lang/ast/emit/PHP80.class.php | 2 ++ 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 ++ src/main/php/lang/ast/emit/RewriteProperties.class.php | 8 +++++--- 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 72a60b59..6f97959c 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -28,6 +28,8 @@ class PHP74 extends PHP { RewriteThrowableExpressions ; + public $targetVersion= 70400; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 5cd927b8..db83e745 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -31,6 +31,8 @@ class PHP80 extends PHP { RewriteStaticVariableInitializations ; + public $targetVersion= 80000; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index de5db7dc..f93b4969 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -28,6 +28,8 @@ class PHP81 extends PHP { OmitConstantTypes ; + public $targetVersion= 80100; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 8d0c1b96..d66d1016 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -28,6 +28,8 @@ class PHP82 extends PHP { OmitConstantTypes ; + public $targetVersion= 80200; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 7ae725f7..f33ef341 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -20,6 +20,8 @@ class PHP83 extends PHP { use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses; + public $targetVersion= 80300; + /** Sets up type => literal mappings */ public function __construct() { $this->literals= [ diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php index d0a50e81..3073aad9 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -10,15 +10,17 @@ trait RewriteProperties { } protected function emitProperty($result, $property) { - static $asymmetric= null; if ($property->hooks) { return $this->emitPropertyHooks($result, $property); } else if ( - !($asymmetric ?? $asymmetric= method_exists(ReflectionProperty::class, 'isPrivateSet')) && + $this->targetVersion < 80400 && array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) ) { return $this->emitAsymmetricVisibility($result, $property); - } else if (PHP_VERSION_ID <= 80100 && in_array('readonly', $property->modifiers)) { + } else if ( + $this->targetVersion < 80100 && + in_array('readonly', $property->modifiers) + ) { return $this->emitReadonlyProperties($result, $property); } parent::emitProperty($result, $property); From 1db0df41cb69bc575771fcb6342ea17f627dc232 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 26 Aug 2024 21:25:23 +0200 Subject: [PATCH 878/926] Use xp-framework/reflection release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5dcb4c8d..16ea0d27 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", - "xp-framework/reflection": "dev-feature/asymmetric-visibility as 3.2.0", + "xp-framework/reflection": "^3.2", "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", "php" : ">=7.4.0" }, From e4a24c023dd85b3f620b3e2cc142bf33e9c136e2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 26 Aug 2024 21:30:08 +0200 Subject: [PATCH 879/926] Use xp-framework/ast release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16ea0d27..60da5ba3 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.2", - "xp-framework/ast": "dev-feature/asymmetric-visibility as 11.3.0", + "xp-framework/ast": "^11.3", "php" : ">=7.4.0" }, "require-dev" : { From 8a578bbff18794a188122dff57ec677028774f02 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 11:51:19 +0200 Subject: [PATCH 880/926] Release 9.1.1 --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 97020c13..3045dfc0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.1.1 / 2024-08-27 + +* Forward compatibility with newer `xp-framework/ast` releases - @thekid + ## 9.1.0 / 2024-06-15 * Merged PR #166: Implement property hooks via virtual properties, see From f55a373cb3397abbe9ab63400c8701f5ed29b7ff Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 20:22:33 +0200 Subject: [PATCH 881/926] Fix readonly tests --- composer.json | 2 +- .../php/lang/ast/unittest/emit/ReadonlyTest.class.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ed40d3f1..7f815af6 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", - "xp-framework/reflection": "^3.0 | ^2.13", + "xp-framework/reflection": "^3.2 | ^2.15", "xp-framework/ast": "^11.1", "php" : ">=7.4.0" }, diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 9c8d1db5..a998a7dd 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -27,7 +27,7 @@ public function class_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -39,7 +39,7 @@ public function property_declaration() { }'); Assert::equals( - 'public readonly int $fixture', + 'public readonly protected(set) int $fixture', $t->property('fixture')->toString() ); } @@ -51,7 +51,7 @@ public function __construct(public string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); @@ -64,7 +64,7 @@ public function __construct(public readonly string $fixture) { } }'); Assert::equals( - 'public readonly string $fixture', + 'public readonly protected(set) string $fixture', $t->property('fixture')->toString() ); Assert::equals('Test', $t->newInstance('Test')->fixture); From 31eac561a8ddd72c5f6e3eb62c3b6c499fcadad5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 20:25:52 +0200 Subject: [PATCH 882/926] Adjust modifier bits to reflection library See https://github.com/xp-framework/compiler/pull/183#discussion_r1733311942 --- .../ast/emit/AsymmetricVisibility.class.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 8daac83a..9bf53ce0 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -28,9 +28,9 @@ protected function emitProperty($result, $property) { 'final' => MODIFIER_FINAL, 'abstract' => MODIFIER_ABSTRACT, 'readonly' => MODIFIER_READONLY, - 'public(set)' => 0x0400, - 'protected(set)' => 0x0800, - 'private(set)' => 0x1000, + 'public(set)' => 0x1000000, + 'protected(set)' => 0x0000800, + 'private(set)' => 0x0001000, ]; $scope= $result->codegen->scope[0]; @@ -41,15 +41,15 @@ protected function emitProperty($result, $property) { // Declare checks for private(set) and protected(set), folding declarations // like `[visibility] [visibility](set)` to just the visibility itself. - if ($modifiers & 0x0400) { + if ($modifiers & 0x1000000) { $checks= []; - $modifiers&= ~0x0400; - } else if ($modifiers & 0x0800) { + $modifiers&= ~0x1000000; + } else if ($modifiers & 0x0000800) { $checks= [$this->protected($property->name, 'modify protected(set)')]; - $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0800; - } else if ($modifiers & 0x1000) { + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; + } else if ($modifiers & 0x0001000) { $checks= [$this->private($property->name, 'modify private(set)')]; - $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x1000; + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; } // Emit XP meta information for the reflection API From a1677a75a875072f03fe3ec2f5328c9edb40558c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 20:29:39 +0200 Subject: [PATCH 883/926] Release 9.2.0 --- ChangeLog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3045dfc0..0f1a291a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.2.0 / 2024-08-27 + +* Merged PR #183: Add emitting support for asymmetric visibility. See + https://wiki.php.net/rfc/asymmetric-visibility-v2, targeted for PHP 8.4 + (@thekid) + ## 9.1.1 / 2024-08-27 * Forward compatibility with newer `xp-framework/ast` releases - @thekid From 3191cbbb4078e3eba22f4ff3c3e1eb8f5c6bc16e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 21:54:16 +0200 Subject: [PATCH 884/926] Remove check for IDisposable, this was added to core all the way back in 9.4.0 --- src/main/php/module.xp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 82f404aa..9517b43e 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -8,9 +8,5 @@ module xp-framework/compiler { /** @return void */ public function initialize() { ClassLoader::registerLoader(CompilingClassloader::instanceFor('php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION)); - - if (!interface_exists(\IDisposable::class, false)) { - eval('interface IDisposable { public function __dispose(); }'); - } } } \ No newline at end of file From 2c2c6d9ddb4366f56f4f6dd531a514f3e106bac6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 22:18:53 +0200 Subject: [PATCH 885/926] Emit readonly classes natively in PHP 8.3 --- src/main/php/lang/ast/emit/PHP83.class.php | 2 +- src/main/php/lang/ast/emit/ReadonlyClasses.class.php | 6 ++++-- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 7 ++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index f33ef341..7c7293b2 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, RewriteProperties, ReadonlyClasses; + use RewriteBlockLambdaExpressions, RewriteProperties; public $targetVersion= 80300; diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index e24d11ab..073f30cc 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -33,9 +33,11 @@ protected function emitClass($result, $class) { } // Prevent dynamic members - $throw= new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);'); $context= $result->codegen->enter(new InType($class)); - $context->virtual[null]= [$throw, $throw]; + $context->virtual[null]= [ + new Code(''), + new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);') + ]; } return parent::emitClass($result, $class); diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index c65e4a66..1ee2ffb8 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -174,10 +174,11 @@ public function cannot_have_an_initial_value() { }'); } - #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] - public function cannot_read_dynamic_members_from_readonly_classes() { + #[Test] + public function reading_dynamic_members_from_readonly_classes_causes_warning() { $t= $this->declare('readonly class %T { }'); - $t->newInstance()->fixture; + Assert::null($t->newInstance()->fixture); + \xp::gc(); } #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] From edaf89c2a91a074d5027d44dd5d81ba5af797f44 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 22:18:53 +0200 Subject: [PATCH 886/926] Emit readonly classes natively in PHP 8.3 --- ChangeLog.md | 4 ++++ src/main/php/lang/ast/emit/PHP83.class.php | 2 +- src/main/php/lang/ast/emit/ReadonlyClasses.class.php | 6 ++++-- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 7 ++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0f1a291a..8022aecb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Changed emitter to use native readonly classes in PHP 8.3, fixing an + inconsistency with accessing undefined properties + (@thekid) + ## 9.2.0 / 2024-08-27 * Merged PR #183: Add emitting support for asymmetric visibility. See diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index f33ef341..7c7293b2 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, RewriteProperties, ReadonlyClasses; + use RewriteBlockLambdaExpressions, RewriteProperties; public $targetVersion= 80300; diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php index e24d11ab..a9aa445b 100755 --- a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -33,9 +33,11 @@ protected function emitClass($result, $class) { } // Prevent dynamic members - $throw= new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);'); $context= $result->codegen->enter(new InType($class)); - $context->virtual[null]= [$throw, $throw]; + $context->virtual[null]= [ + new Code('trigger_error("Undefined property: ".__CLASS__."::\$".$name, E_USER_WARNING); return $_;'), + new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);') + ]; } return parent::emitClass($result, $class); diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index c65e4a66..1ee2ffb8 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -174,10 +174,11 @@ public function cannot_have_an_initial_value() { }'); } - #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] - public function cannot_read_dynamic_members_from_readonly_classes() { + #[Test] + public function reading_dynamic_members_from_readonly_classes_causes_warning() { $t= $this->declare('readonly class %T { }'); - $t->newInstance()->fixture; + Assert::null($t->newInstance()->fixture); + \xp::gc(); } #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] From 8ba8a15c355c14c9e0a8c059f31acb4ab8972558 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 27 Aug 2024 22:29:43 +0200 Subject: [PATCH 887/926] Document PHP 8.4 emitter --- ChangeLog.md | 4 ++++ README.md | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8022aecb..83ba14b2 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Added PHP 8.4 emitter which feature-checks for both property hooks + and asymmetric visibility RFCs, deciding whether to natively emit or + use virtual properties + (@thekid) * Changed emitter to use native readonly classes in PHP 8.3, fixing an inconsistency with accessing undefined properties (@thekid) diff --git a/README.md b/README.md index c3f90b34..c6fd9e35 100755 --- a/README.md +++ b/README.md @@ -91,8 +91,9 @@ Usage: xp compile [] lang.ast.emit.PHP74 lang.ast.emit.PHP80 lang.ast.emit.PHP81 -lang.ast.emit.PHP82 [*] -lang.ast.emit.PHP83 +lang.ast.emit.PHP82 +lang.ast.emit.PHP83 [*] +lang.ast.emit.PHP84 lang.ast.syntax.php.Using [*] @FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> From 12ae79ab204891a33c351fb55570bd791ed8331b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 31 Aug 2024 10:38:49 +0200 Subject: [PATCH 888/926] Adjust expected error message to PHP 8.4-dev --- src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php index 1ee2ffb8..9b9830fb 100755 --- a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -151,7 +151,7 @@ public function can_be_assigned_via_reflection() { Assert::equals('Test', $i->fixture); } - #[Test, Expect(class: Error::class, message: '/Cannot (initialize readonly|modify protected\(set\)) property .+fixture/')] + #[Test, Expect(class: Error::class, message: '/Cannot (initialize readonly|modify protected\(set\) readonly) property .+fixture/')] public function cannot_initialize_from_outside() { $t= $this->declare('class %T { public readonly string $fixture; From ab07d7c971877b9eb6532c3be0e1fefce1660f64 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 31 Aug 2024 10:40:16 +0200 Subject: [PATCH 889/926] Remove emitProperty() following hooks and asymmetric visibility are both merged See https://github.com/xp-framework/compiler/issues/182#issuecomment-2313491231 --- src/main/php/lang/ast/emit/PHP84.class.php | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php index a23910d5..71379f30 100755 --- a/src/main/php/lang/ast/emit/PHP84.class.php +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -1,6 +1,5 @@ function($t) { return null; } ]; } - - protected function emitProperty($result, $property) { - static $asymmetric= null; - static $hooks= null; - - // TODO Remove once https://github.com/php/php-src/pull/15063 and - // https://github.com/php/php-src/pull/13455 are merged - if ( - !($asymmetric ?? $asymmetric= method_exists(ReflectionProperty::class, 'isPrivateSet')) && - array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) - ) { - return $this->emitAsymmetricVisibility($result, $property); - } else if ( - !($hooks ?? $hooks= method_exists(ReflectionProperty::class, 'getHooks')) && - $property->hooks - ) { - return $this->emitPropertyHooks($result, $property); - } - - parent::emitProperty($result, $property); - } } \ No newline at end of file From 1428196c908fb1812f0931a77210696941f6c8fb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 31 Aug 2024 11:49:51 +0200 Subject: [PATCH 890/926] Test native hooks / asymmetric visibility emitter for PHP 8.4 --- ChangeLog.md | 5 ++-- .../ast/unittest/emit/PHP84Test.class.php | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/PHP84Test.class.php diff --git a/ChangeLog.md b/ChangeLog.md index 83ba14b2..3166c6e7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,9 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? -* Added PHP 8.4 emitter which feature-checks for both property hooks - and asymmetric visibility RFCs, deciding whether to natively emit or - use virtual properties +* Added PHP 8.4 emitter which natively emits property hooks and asymmetric + visibility syntax (@thekid) * Changed emitter to use native readonly classes in PHP 8.3, fixing an inconsistency with accessing undefined properties diff --git a/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php new file mode 100755 index 00000000..69c55acd --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php @@ -0,0 +1,26 @@ +"Test";}/', + $this->emit('class %T { public $test { get => "Test"; } }') + ); + } + + #[Test] + public function asymmetric_visibility() { + Assert::matches( + '/public private\(set\) string \$test/', + $this->emit('class %T { public private(set) string $test; }') + ); + } +} \ No newline at end of file From b0c6462aa649dea0aabd0a1139215d7aff2691c7 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 31 Aug 2024 20:15:14 +0200 Subject: [PATCH 891/926] Emit readonly classes natively with PHP 8.2 See https://wiki.php.net/rfc/readonly_classes --- ChangeLog.md | 2 +- src/main/php/lang/ast/emit/PHP82.class.php | 1 - src/test/php/lang/ast/unittest/emit/PHP82Test.class.php | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 3166c6e7..bc39f7da 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,7 +6,7 @@ XP Compiler ChangeLog * Added PHP 8.4 emitter which natively emits property hooks and asymmetric visibility syntax (@thekid) -* Changed emitter to use native readonly classes in PHP 8.3, fixing an +* Changed emitter to use native readonly classes in PHP 8.2, fixing an inconsistency with accessing undefined properties (@thekid) diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index d66d1016..89fa9cbd 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -24,7 +24,6 @@ class PHP82 extends PHP { RewriteDynamicClassConstants, RewriteStaticVariableInitializations, RewriteProperties, - ReadonlyClasses, OmitConstantTypes ; diff --git a/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php index f2818174..089efb86 100755 --- a/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php +++ b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php @@ -22,4 +22,12 @@ public function callable_static_method_syntax() { public function callable_instance_method_syntax() { Assert::equals('$this->method(...);', $this->emit('$this->method(...);')); } + + #[Test] + public function readonly_classes() { + Assert::matches( + '/readonly class [A-Z0-9]+{/', + $this->emit('readonly class %T { }') + ); + } } \ No newline at end of file From 2a95ebb4c95c32244f4871cf0028bbe69daf0417 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 31 Aug 2024 20:19:00 +0200 Subject: [PATCH 892/926] Release 9.3.0 --- ChangeLog.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index bc39f7da..fde4fcff 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,8 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.3.0 / 2024-08-31 + * Added PHP 8.4 emitter which natively emits property hooks and asymmetric - visibility syntax + visibility syntax. This is integration-tested with PHP 8.4.0 Beta 4. + See https://github.com/php/php-src/blob/php-8.4.0beta4/NEWS (@thekid) * Changed emitter to use native readonly classes in PHP 8.2, fixing an inconsistency with accessing undefined properties From f37388f5317b050b7a43101765e90e56da5955a6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 1 Sep 2024 15:02:01 +0200 Subject: [PATCH 893/926] Fix checks for property hooks emulation with asymmetric visibility --- ChangeLog.md | 2 ++ .../php/lang/ast/emit/PropertyHooks.class.php | 35 ++++++++++++++----- .../emit/AsymmetricVisibilityTest.class.php | 18 ++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index fde4fcff..57b04c0f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,8 @@ XP Compiler ChangeLog ## 9.3.0 / 2024-08-31 +* Fixed checks for property hooks emulation with asymmetric visibility + (@thekid) * Added PHP 8.4 emitter which natively emits property hooks and asymmetric visibility syntax. This is integration-tested with PHP 8.4.0 Beta 4. See https://github.com/php/php-src/blob/php-8.4.0beta4/NEWS diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 310226f8..7f76d509 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -72,13 +72,16 @@ protected function withScopeCheck($modifiers, $nodes) { protected function emitProperty($result, $property) { static $lookup= [ - 'public' => MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => MODIFIER_READONLY, + 'public' => MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => MODIFIER_READONLY, + 'public(set)' => 0x1000000, + 'protected(set)' => 0x0000800, + 'private(set)' => 0x0001000, ]; // Emit XP meta information for the reflection API @@ -88,6 +91,22 @@ protected function emitProperty($result, $property) { $modifiers|= $lookup[$name]; } + // Derive modifiers for private(set) and protected(set), folding declarations + // like `[visibility] [visibility](set)` to just the visibility itself. + if ($modifiers & 0x1000000) { + $check= null; + $modifiers&= ~0x1000000; + $write= MODIFIER_PUBLIC; + } else if ($modifiers & 0x0000800) { + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; + $write= MODIFIER_PROTECTED; + } else if ($modifiers & 0x0001000) { + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; + $write= MODIFIER_PRIVATE; + } else { + $write= $modifiers; + } + $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, @@ -137,7 +156,7 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $set= $this->withScopeCheck($modifiers, [new InvokeExpression( + $set= $this->withScopeCheck($write, [new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [new Variable('value')] )]); diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 1c327efe..1007425e 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -123,4 +123,22 @@ public function same_modifier_for_get_and_set($modifier) { $t->property('fixture')->toString() ); } + + #[Test] + public function interaction_with_hooks() { + $t= $this->declare('class %T { + public private(set) string $fixture { + get => $this->fixture; + set => strtolower($value); + } + + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('changed', $t->newInstance()->rename('Changed')->fixture); + } } \ No newline at end of file From 07dd3b414646384a259103e7a4b9e98f18da1307 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2024 19:28:00 +0200 Subject: [PATCH 894/926] Fix enclosing scopes when using references --- ChangeLog.md | 2 ++ src/main/php/lang/ast/emit/PHP.class.php | 16 +++++++-------- .../emit/ControlStructuresTest.class.php | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 57b04c0f..ce98002c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed enclosing scopes when using references - @thekid + ## 9.3.0 / 2024-08-31 * Fixed checks for property hooks emulation with asymmetric visibility diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f4445bfe..85253045 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -125,7 +125,7 @@ protected function enclose($result, $node, $signature, $static, $emit) { $capture= []; foreach ($result->codegen->search($node, 'variable') as $var) { if (isset($result->locals[$var->pointer])) { - $capture[$var->pointer]= true; + $capture[$var->pointer]??= ($result->locals[$var->pointer] ? '&$' : '$').$var->pointer; } } unset($capture['this']); @@ -143,9 +143,9 @@ protected function enclose($result, $node, $signature, $static, $emit) { } if ($capture) { - $result->out->write('use($'.implode(', $', array_keys($capture)).')'); - foreach ($capture as $name => $_) { - $result->locals[$name]= true; + $result->out->write('use('.implode(', ', $capture).')'); + foreach ($capture as $name => $variable) { + $result->locals[$name]= '&' === $variable[0]; } } @@ -298,7 +298,7 @@ protected function emitArray($result, $array) { } protected function emitParameter($result, $parameter) { - $result->locals[$parameter->name]= true; + $result->locals[$parameter->name]= $parameter->reference; $parameter->annotations && $this->emitOne($result, $parameter->annotations); // If we have a non-constant default and a type, emit a nullable type hint @@ -340,7 +340,7 @@ protected function emitSignature($result, $signature, $use= null) { if ($use) { $result->out->write(' use('.implode(',', $use).') '); foreach ($use as $variable) { - $result->locals[substr($variable, 1)]= true; + $result->locals[ltrim($variable, '&$')]= '&' === $variable[0]; } } @@ -675,7 +675,7 @@ protected function emitProperty($result, $property) { protected function emitMethod($result, $method) { $locals= $result->locals; - $result->locals= ['this' => true]; + $result->locals= ['this' => false]; $meta= [ DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', DETAIL_ANNOTATIONS => $method->annotations, @@ -798,7 +798,7 @@ protected function emitOffset($result, $offset) { protected function emitAssign($result, $target) { if ($target instanceof Variable && $target->const) { $result->out->write('$'.$target->pointer); - $result->locals[$target->pointer]= true; + $result->locals[$target->pointer]= false; } else if ($target instanceof ArrayLiteral) { $result->out->write('['); foreach ($target->values as $pair) { diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index c42344c4..81cb042b 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -211,4 +211,24 @@ public function run() { Assert::equals('one item', $r(1)); } + + #[Test] + public function match_block_inside_function_using_ref() { + $r= $this->run('class %T { + public function run() { + $test= "Original"; + (function() use(&$test) { + match (true) { + true => { + $test= "Changed"; + return true; + } + }; + })(); + return $test; + } + }'); + + Assert::equals('Changed', $r); + } } \ No newline at end of file From e772b3d3b51773c08bcdb617ac604c339b60ad20 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 4 Oct 2024 19:29:39 +0200 Subject: [PATCH 895/926] Add test for readonly classes emitted in PHP 8.3 --- .../lang/ast/unittest/emit/PHP83Test.class.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/PHP83Test.class.php diff --git a/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php new file mode 100755 index 00000000..16197321 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php @@ -0,0 +1,18 @@ +emit('readonly class %T { }') + ); + } +} \ No newline at end of file From 0b7c768b5f0f4ca491e69b0354909ffa633b3e61 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Oct 2024 09:19:16 +0200 Subject: [PATCH 896/926] Fix `private(set)` not being implicitely marked as final See https://wiki.php.net/rfc/asymmetric-visibility-v2#inheritance --- ChangeLog.md | 3 ++ .../ast/emit/AsymmetricVisibility.class.php | 1 + .../emit/AsymmetricVisibilityTest.class.php | 34 ++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ce98002c..7425109f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Fixed `private(set)` not being implicitely marked as *final*, see + https://wiki.php.net/rfc/asymmetric-visibility-v2#inheritance + (@thekid) * Fixed enclosing scopes when using references - @thekid ## 9.3.0 / 2024-08-31 diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 9bf53ce0..721c4c3c 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -50,6 +50,7 @@ protected function emitProperty($result, $property) { } else if ($modifiers & 0x0001000) { $checks= [$this->private($property->name, 'modify private(set)')]; $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; + $modifiers|= MODIFIER_FINAL; } // Emit XP meta information for the reflection API diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 1007425e..9de9d07c 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -100,19 +100,31 @@ public function rename() { $t->newInstance()->rename(); } - #[Test, Values(['private', 'protected'])] - public function reflection($modifier) { + #[Test] + public function protected_set_reflection() { $t= $this->declare('class %T { - public '.$modifier.'(set) string $fixture= "Test"; + public protected(set) string $fixture= "Test"; }'); Assert::equals( - 'public '.$modifier.'(set) string $fixture', + 'public protected(set) string $fixture', $t->property('fixture')->toString() ); } - #[Test, Values(['private', 'protected', 'public'])] + #[Test] + public function private_set_implicitely_final_in_reflection() { + $t= $this->declare('class %T { + public private(set) string $fixture= "Test"; + }'); + + Assert::equals( + 'public final private(set) string $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test, Values(['protected', 'public'])] public function same_modifier_for_get_and_set($modifier) { $t= $this->declare('class %T { '.$modifier.' '.$modifier.'(set) string $fixture= "Test"; @@ -124,6 +136,18 @@ public function same_modifier_for_get_and_set($modifier) { ); } + #[Test] + public function private_modifier_for_get_and_set() { + $t= $this->declare('class %T { + private private(set) string $fixture= "Test"; + }'); + + Assert::equals( + 'private final string $fixture', + $t->property('fixture')->toString() + ); + } + #[Test] public function interaction_with_hooks() { $t= $this->declare('class %T { From ccc88206368481a4339a105b8f5b9b623752a815 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Oct 2024 09:38:05 +0200 Subject: [PATCH 897/926] Add more tests for private(set) properties implicit final --- .../ast/emit/AsymmetricVisibility.class.php | 3 +-- .../emit/AsymmetricVisibilityTest.class.php | 22 +++++-------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index 721c4c3c..f277e228 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -49,8 +49,7 @@ protected function emitProperty($result, $property) { $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; } else if ($modifiers & 0x0001000) { $checks= [$this->private($property->name, 'modify private(set)')]; - $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; - $modifiers|= MODIFIER_FINAL; + $modifiers & MODIFIER_PRIVATE ? $modifiers&= ~0x0001000 : $modifiers|= MODIFIER_FINAL; } // Emit XP meta information for the reflection API diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php index 9de9d07c..b72b9ae5 100755 --- a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -112,19 +112,19 @@ public function protected_set_reflection() { ); } - #[Test] - public function private_set_implicitely_final_in_reflection() { + #[Test, Values(['protected', 'public'])] + public function private_set_implicitely_final_in_reflection($modifier) { $t= $this->declare('class %T { - public private(set) string $fixture= "Test"; + '.$modifier.' private(set) string $fixture= "Test"; }'); Assert::equals( - 'public final private(set) string $fixture', + $modifier.' final private(set) string $fixture', $t->property('fixture')->toString() ); } - #[Test, Values(['protected', 'public'])] + #[Test, Values(['private', 'protected', 'public'])] public function same_modifier_for_get_and_set($modifier) { $t= $this->declare('class %T { '.$modifier.' '.$modifier.'(set) string $fixture= "Test"; @@ -136,18 +136,6 @@ public function same_modifier_for_get_and_set($modifier) { ); } - #[Test] - public function private_modifier_for_get_and_set() { - $t= $this->declare('class %T { - private private(set) string $fixture= "Test"; - }'); - - Assert::equals( - 'private final string $fixture', - $t->property('fixture')->toString() - ); - } - #[Test] public function interaction_with_hooks() { $t= $this->declare('class %T { From 4d0ded8bc687b78f4ff1490c807f480c1bf4b259 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Oct 2024 09:38:47 +0200 Subject: [PATCH 898/926] Release 9.3.1 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 7425109f..a2b75c5d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.3.1 / 2024-10-05 + * Fixed `private(set)` not being implicitely marked as *final*, see https://wiki.php.net/rfc/asymmetric-visibility-v2#inheritance (@thekid) From 11b7f1a5cc5f1d985f792ad3e13a2d7d83e69914 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Nov 2024 11:47:09 +0100 Subject: [PATCH 899/926] Add tests for non-constant parameter defaults See also: * https://wiki.php.net/rfc/new_in_initializers * https://wiki.php.net/rfc/closures_in_const_expr --- .../ast/unittest/emit/ParameterTest.class.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index fc836c0c..da9a4174 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -3,6 +3,7 @@ use lang\{ArrayType, MapType, Primitive, Type, Value, XPClass}; use test\verify\Runtime; use test\{Action, Assert, Test, Values}; +use util\Binford; class ParameterTest extends EmittingTest { use AnnotationsOf, NullableSupport; @@ -14,7 +15,7 @@ class ParameterTest extends EmittingTest { * @return lang.reflection.Parameter */ private function param($declaration) { - return $this->declare('use lang\Value; class %T { public function fixture('.$declaration.') { } }') + return $this->declare('use lang\Value; use util\Binford; class %T { public function fixture('.$declaration.') { } }') ->method('fixture') ->parameter(0) ; @@ -122,6 +123,18 @@ public function optional_parameters_default_value() { Assert::equals(true, $this->param('$param= true')->default()); } + #[Test] + public function new_as_default() { + $power= $this->param('$power= new Binford(6100)')->default(); + Assert::equals(new Binford(6100), $power); + } + + #[Test] + public function closure_as_default() { + $function= $this->param('$op= fn($in) => $in * 2')->default(); + Assert::equals(2, $function(1)); + } + #[Test] public function trailing_comma_allowed() { $p= $this->declare('class %T { public function fixture($param, ) { } }') From 268c79e74ba73fb8188c0edff477d3fbaf5240ef Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Nov 2024 11:48:58 +0100 Subject: [PATCH 900/926] Verify first-class callables can be used as parameter defaults See https://wiki.php.net/rfc/closures_in_const_expr#future_scope --- src/test/php/lang/ast/unittest/emit/ParameterTest.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index da9a4174..14fd6efc 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -135,6 +135,12 @@ public function closure_as_default() { Assert::equals(2, $function(1)); } + #[Test] + public function first_class_callable_as_default() { + $function= $this->param('$op= strlen(...)')->default(); + Assert::equals(4, $function('Test')); + } + #[Test] public function trailing_comma_allowed() { $p= $this->declare('class %T { public function fixture($param, ) { } }') From c60c1e2b5ade2fad15831c7171cc503b0b126127 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Nov 2024 11:55:38 +0100 Subject: [PATCH 901/926] Verify first-class callables work in annotations See https://wiki.php.net/rfc/closures_in_const_expr#future_scope --- .../php/lang/ast/unittest/emit/AnnotationSupport.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php index fd566b36..d2599d02 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -74,6 +74,12 @@ public function closure_value() { Assert::equals('test', $verify[0]('test')); } + #[Test] + public function first_class_callable_value() { + $verify= $this->annotations($this->declare('#[Verify(strtoupper(...))]'))['Verify']; + Assert::equals('TEST', $verify[0]('test')); + } + #[Test] public function arrow_function_value() { $verify= $this->annotations($this->declare('#[Verify(fn($arg) => $arg)]'))['Verify']; From 1ae3ccb207f7fefe2cefaac0293df63077d7b808 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 2 Nov 2024 17:32:05 +0100 Subject: [PATCH 902/926] Fix empty match expressions and match expressions with default case only in PHP 7 --- ChangeLog.md | 9 +++++++ .../lang/ast/emit/MatchAsTernaries.class.php | 5 +++- .../emit/ControlStructuresTest.class.php | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index a2b75c5d..7b907256 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,15 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.3.2 / 2024-11-02 + +* Fixed empty match expressions and match expressions with default + case only in PHP 7 + (@thekid) +* Added tests verifying closures are supported in constant expressions + https://wiki.php.net/rfc/closures_in_const_expr + (@thekid) + ## 9.3.1 / 2024-10-05 * Fixed `private(set)` not being implicitely marked as *final*, see diff --git a/src/main/php/lang/ast/emit/MatchAsTernaries.class.php b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php index 6d6da1df..9e82693d 100755 --- a/src/main/php/lang/ast/emit/MatchAsTernaries.class.php +++ b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php @@ -30,9 +30,12 @@ protected function emitMatch($result, $match) { } } + // Match without cases => create something that will never match + $b || $result->out->write('===NAN?:'); + // Emit IIFE for raising an error until we have throw expressions if (null === $match->default) { - $result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })('); + $result->out->write('(function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })()'); } else { $this->emitAsExpression($result, $match->default); } diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index 81cb042b..299374a0 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -86,6 +86,19 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test] + public function match_with_default_only() { + $r= $this->run('class %T { + public function run() { + return match (true) { + default => "Test", + }; + } + }'); + + Assert::equals('Test', $r); + } + #[Test, Values([[200, 'OK'], [302, 'Redirect'], [404, 'Error #404']])] public function match_with_multiple_cases($input, $expected) { $r= $this->run('class %T { @@ -154,6 +167,17 @@ public function run($arg) { Assert::equals('10+ items', $r); } + #[Test, Expect(class: Throwable::class, message: '/Unhandled match (value of type .+|case .+)/')] + public function empty_match() { + $r= $this->run('class %T { + public function run() { + return match (true) { }; + } + }'); + + Assert::equals('Test', $r); + } + #[Test, Expect(class: Throwable::class, message: '/Unhandled match (value of type .+|case .+)/')] public function unhandled_match() { $this->run('class %T { From 3bf4ccbfa133e2cf44dd98954fd83c7fa1d096dc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 25 Jan 2025 11:52:23 +0100 Subject: [PATCH 903/926] Add test for using first-class callable syntax inside annotations See https://wiki.php.net/rfc/fcc_in_const_expr#examples --- .../ast/unittest/emit/CallableSyntaxTest.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index e9211bb9..5baba689 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -6,6 +6,7 @@ /** * Tests for first-class callable syntax * + * @see https://wiki.php.net/rfc/fcc_in_const_expr * @see https://wiki.php.net/rfc/first_class_callable_syntax#proposal */ class CallableSyntaxTest extends EmittingTest { @@ -182,4 +183,16 @@ public function __construct($value) { $this->value= $value; } }'); Assert::equals($this, $f($this)->value); } + + #[Test] + public function inside_annotation() { + $f= $this->run('use lang\Reflection; class %T { + + #[Attr(strrev(...))] + public function run() { + return Reflection::of($this)->method("run")->annotation(Attr::class)->argument(0); + } + }'); + Assert::equals('cba', $f('abc')); + } } \ No newline at end of file From 4b3514e0f4944985804fcfcd7cad4cbad4beaf31 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 26 Jan 2025 12:42:39 +0100 Subject: [PATCH 904/926] Fix uncaught exceptions during cleanup XP xar support holds on to file handles --- .../php/lang/ast/unittest/cli/ToArchiveTest.class.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php index 53c19921..f65f822c 100755 --- a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php +++ b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php @@ -1,6 +1,6 @@ archive->isOpen() && $this->archive->close(); - $this->folder->unlink(); + try { + $this->folder->unlink(); + } catch (IOException $ignored) { + // XP xar support holds on to file handles + } } #[Test] From ff28975cf0a3c69756df61ef818e8f916c4092ed Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Feb 2025 09:59:48 +0100 Subject: [PATCH 905/926] Link to https://wiki.php.net/rfc/pipe-operator-v3 --- .../ast/unittest/emit/PipelinesTest.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index 8e5dbeec..fdcb1231 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -2,6 +2,7 @@ use test\{Assert, Test, Values}; +/** @see https://wiki.php.net/rfc/pipe-operator-v3 */ class PipelinesTest extends EmittingTest { #[Test] @@ -129,4 +130,19 @@ public function run($arg) { Assert::equals($expected, $r); } + + #[Test] + public function rfc_example() { + $r= $this->run('class %T { + public function run() { + return "Hello World" + |> "htmlentities" + |> str_split(...) + |> fn($x) => array_map(strtoupper(...), $x) + |> fn($x) => array_filter($x, fn($v) => $v != "O") + ; + } + }'); + Assert::equals(['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D'], array_values($r)); + } } \ No newline at end of file From a7c7d79949f628d15cfe1cb35d7cbaf4c30a0616 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 8 Feb 2025 10:02:11 +0100 Subject: [PATCH 906/926] Test precedence, see https://wiki.php.net/rfc/pipe-operator-v3#precedence --- .../lang/ast/unittest/emit/PipelinesTest.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index fdcb1231..b18915a0 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -131,6 +131,17 @@ public function run($arg) { Assert::equals($expected, $r); } + #[Test] + public function precedence() { + $r= $this->run('class %T { + public function run() { + return "te"."st" |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + #[Test] public function rfc_example() { $r= $this->run('class %T { From 02af2ea9203cf78c5484dbacd85e37735644bfb5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 2 Mar 2025 10:35:50 +0100 Subject: [PATCH 907/926] Fix callable new syntax when using a variable or expression See also https://github.com/php/php-src/issues/12336 --- ChangeLog.md | 6 +++++ src/main/php/lang/ast/emit/PHP.class.php | 4 +--- .../emit/CallableSyntaxTest.class.php | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 7b907256..bea236f0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.3.3 / 2025-03-02 + +* Fixed callable new syntax when using a variable or expression, e.g. + `new $class(...)`. See also https://github.com/php/php-src/issues/12336 + (@thekid) + ## 9.3.2 / 2024-11-02 * Fixed empty match expressions and match expressions with default diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 85253045..63c5fed8 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1096,13 +1096,11 @@ protected function emitCallable($result, $callable) { protected function emitCallableNew($result, $callable) { $t= $result->temp(); - $result->out->write("function(...{$t}) { return "); + $result->out->write("fn(...{$t}) => "); $callable->type->arguments= [new UnpackExpression(new Variable(substr($t, 1)), $callable->line)]; $this->emitOne($result, $callable->type); $callable->type->arguments= null; - - $result->out->write("; }"); } protected function emitInvoke($result, $invoke) { diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php index 5baba689..f12f5038 100755 --- a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -171,6 +171,28 @@ public function run() { Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); } + #[Test] + public function variable_instantiation() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + $class= Handle::class; + return array_map(new $class(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + + #[Test] + public function expression_instantiation() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + $version= ""; + return array_map(new (Handle::class.$version)(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + #[Test] public function anonymous_instantiation() { $f= $this->run('class %T { From 0efd60e57e2fbeb4da3df27df69772d4aac5391d Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 15:17:30 +0200 Subject: [PATCH 908/926] Add support for final properties --- .../lang/ast/emit/FinalProperties.class.php | 25 +++++++++++++++++++ .../lang/ast/emit/RewriteProperties.class.php | 8 +++++- .../emit/ArgumentPromotionTest.class.php | 10 ++++++++ .../ast/unittest/emit/MembersTest.class.php | 9 +++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100755 src/main/php/lang/ast/emit/FinalProperties.class.php diff --git a/src/main/php/lang/ast/emit/FinalProperties.class.php b/src/main/php/lang/ast/emit/FinalProperties.class.php new file mode 100755 index 00000000..fc2349af --- /dev/null +++ b/src/main/php/lang/ast/emit/FinalProperties.class.php @@ -0,0 +1,25 @@ + MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY + ]; + + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } + + $property->modifiers= array_diff($property->modifiers, ['final']); + parent::emitProperty($result, $property); + $result->codegen->scope[0]->meta[self::PROPERTY][$property->name][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 index 3073aad9..72102e9b 100755 --- a/src/main/php/lang/ast/emit/RewriteProperties.class.php +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -3,8 +3,9 @@ use ReflectionProperty; trait RewriteProperties { - use PropertyHooks, ReadonlyProperties, AsymmetricVisibility { + use PropertyHooks, FinalProperties, ReadonlyProperties, AsymmetricVisibility { PropertyHooks::emitProperty as emitPropertyHooks; + FinalProperties::emitProperty as emitFinalProperties; ReadonlyProperties::emitProperty as emitReadonlyProperties; AsymmetricVisibility::emitProperty as emitAsymmetricVisibility; } @@ -17,6 +18,11 @@ protected function emitProperty($result, $property) { array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) ) { return $this->emitAsymmetricVisibility($result, $property); + } else if ( + $this->targetVersion < 80400 && + in_array('final', $property->modifiers) + ) { + return $this->emitFinalProperties($result, $property); } else if ( $this->targetVersion < 80100 && in_array('readonly', $property->modifiers) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 26481133..6ea0214f 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -10,6 +10,7 @@ * @see https://github.com/xp-framework/rfc/issues/240 * @see https://docs.hhvm.com/hack/other-features/constructor-parameter-promotion * @see https://wiki.php.net/rfc/constructor_promotion (PHP 8.0) + * @see https://wiki.php.net/rfc/final_prompotion * @see https://wiki.php.net/rfc/automatic_property_initialization (Declined) */ class ArgumentPromotionTest extends EmittingTest { @@ -142,4 +143,13 @@ public function __construct(private array $list) { } }'); Assert::equals('Test', $t->newInstance(['Test'])->first); } + + #[Test] + public function promoted_final() { + $t= $this->declare('class %T { + public function __construct(public final string $name) { } + }'); + + Assert::equals(MODIFIER_PUBLIC | MODIFIER_FINAL, $t->property('name')->modifiers()->bits()); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 348aa566..2465cb97 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -425,4 +425,13 @@ public function run() { Assert::equals('Test', $r); } + + #[Test] + public function final_property() { + $t= $this->declare('class %T { + public final string $fixture= "Test"; + }'); + + Assert::equals(MODIFIER_PUBLIC | MODIFIER_FINAL, $t->property('fixture')->modifiers()->bits()); + } } \ No newline at end of file From 82299e29de670f1a4ee69ce005d95765ccd9ca41 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 15:20:48 +0200 Subject: [PATCH 909/926] Optimize xp::$meta --- src/main/php/lang/ast/emit/FinalProperties.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/FinalProperties.class.php b/src/main/php/lang/ast/emit/FinalProperties.class.php index fc2349af..ccbb3532 100755 --- a/src/main/php/lang/ast/emit/FinalProperties.class.php +++ b/src/main/php/lang/ast/emit/FinalProperties.class.php @@ -18,8 +18,9 @@ protected function emitProperty($result, $property) { $modifiers|= $lookup[$name]; } + // Emit without final modifier, store `final` in xp::$meta $property->modifiers= array_diff($property->modifiers, ['final']); parent::emitProperty($result, $property); - $result->codegen->scope[0]->meta[self::PROPERTY][$property->name][DETAIL_ARGUMENTS]= [$modifiers]; + $result->codegen->scope[0]->meta[self::PROPERTY][$property->name][DETAIL_ARGUMENTS]= [MODIFIER_FINAL]; } } \ No newline at end of file From aa8b4e7f6014b8f387a680a0397dd86223961c21 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 15:32:37 +0200 Subject: [PATCH 910/926] Ensure syntactic support for final properties --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b06dd5cf..f98e301e 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.2 | ^2.15", - "xp-framework/ast": "^11.3", + "xp-framework/ast": "^11.5", "php" : ">=7.4.0" }, "require-dev" : { From 8dbdd0484f4b17eed5685bfbf2f9a7f23a70be10 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 16:00:14 +0200 Subject: [PATCH 911/926] Extract modifier mapping to utility class See https://github.com/xp-framework/compiler/pull/184#pullrequestreview-2744901286 --- .../ast/emit/AsymmetricVisibility.class.php | 18 +---------- .../lang/ast/emit/FinalProperties.class.php | 24 +++++---------- .../php/lang/ast/emit/Modifiers.class.php | 30 +++++++++++++++++++ .../php/lang/ast/emit/PropertyHooks.class.php | 19 +----------- .../ast/emit/ReadonlyProperties.class.php | 15 +--------- 5 files changed, 41 insertions(+), 65 deletions(-) create mode 100755 src/main/php/lang/ast/emit/Modifiers.class.php diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php index f277e228..54008a01 100755 --- a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -20,24 +20,8 @@ trait AsymmetricVisibility { use VisibilityChecks; protected function emitProperty($result, $property) { - static $lookup= [ - 'public' => MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => MODIFIER_READONLY, - 'public(set)' => 0x1000000, - 'protected(set)' => 0x0000800, - 'private(set)' => 0x0001000, - ]; - $scope= $result->codegen->scope[0]; - $modifiers= 0; - foreach ($property->modifiers as $name) { - $modifiers|= $lookup[$name]; - } + $modifiers= Modifiers::bits($property->modifiers); // Declare checks for private(set) and protected(set), folding declarations // like `[visibility] [visibility](set)` to just the visibility itself. diff --git a/src/main/php/lang/ast/emit/FinalProperties.class.php b/src/main/php/lang/ast/emit/FinalProperties.class.php index ccbb3532..b5e9851b 100755 --- a/src/main/php/lang/ast/emit/FinalProperties.class.php +++ b/src/main/php/lang/ast/emit/FinalProperties.class.php @@ -1,24 +1,16 @@ MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY - ]; - - $modifiers= 0; - foreach ($property->modifiers as $name) { - $modifiers|= $lookup[$name]; - } - - // Emit without final modifier, store `final` in xp::$meta $property->modifiers= array_diff($property->modifiers, ['final']); parent::emitProperty($result, $property); $result->codegen->scope[0]->meta[self::PROPERTY][$property->name][DETAIL_ARGUMENTS]= [MODIFIER_FINAL]; diff --git a/src/main/php/lang/ast/emit/Modifiers.class.php b/src/main/php/lang/ast/emit/Modifiers.class.php new file mode 100755 index 00000000..7e55b853 --- /dev/null +++ b/src/main/php/lang/ast/emit/Modifiers.class.php @@ -0,0 +1,30 @@ + MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY + 'public(set)' => 0x1000000, + 'protected(set)' => 0x0000800, + 'private(set)' => 0x0001000, + ]; + + /** + * Converts modifiers to a bit set + * + * @param string[] $modifiers + * @return int + */ + public static function bits($modifiers) { + $bits= 0; + foreach ($modifiers as $name) { + $bits|= self::LOOKUP[$name]; + } + return $bits; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 7f76d509..66bce050 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -71,25 +71,8 @@ protected function withScopeCheck($modifiers, $nodes) { } protected function emitProperty($result, $property) { - static $lookup= [ - 'public' => MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => MODIFIER_READONLY, - 'public(set)' => 0x1000000, - 'protected(set)' => 0x0000800, - 'private(set)' => 0x0001000, - ]; - - // Emit XP meta information for the reflection API $scope= $result->codegen->scope[0]; - $modifiers= 0; - foreach ($property->modifiers as $name) { - $modifiers|= $lookup[$name]; - } + $modifiers= Modifiers::bits($property->modifiers); // Derive modifiers for private(set) and protected(set), folding declarations // like `[visibility] [visibility](set)` to just the visibility itself. diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 82530fec..faa65120 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -22,23 +22,10 @@ trait ReadonlyProperties { use VisibilityChecks; protected function emitProperty($result, $property) { - static $lookup= [ - 'public' => MODIFIER_PUBLIC, - 'protected' => MODIFIER_PROTECTED, - 'private' => MODIFIER_PRIVATE, - 'static' => MODIFIER_STATIC, - 'final' => MODIFIER_FINAL, - 'abstract' => MODIFIER_ABSTRACT, - 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY - ]; - if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property); $scope= $result->codegen->scope[0]; - $modifiers= 0; - foreach ($property->modifiers as $name) { - $modifiers|= $lookup[$name]; - } + $modifiers= Modifiers::bits($property->modifiers); $scope->meta[self::PROPERTY][$property->name]= [ DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', DETAIL_ANNOTATIONS => $property->annotations, From 5b4c779a751e812cbb44189ccfe011de38015fec Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 16:03:23 +0200 Subject: [PATCH 912/926] Remove guard clause double-checking for `readonly` --- ChangeLog.md | 5 +++++ src/main/php/lang/ast/emit/ReadonlyProperties.class.php | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index bea236f0..3592cba5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,11 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #184: Add support for final properties added in PHP 8.4, see + https://wiki.php.net/rfc/property-hooks; including parameter promotion, + see https://wiki.php.net/rfc/final_promotion + (@thekid) + ## 9.3.3 / 2025-03-02 * Fixed callable new syntax when using a variable or expression, e.g. diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index faa65120..1ccf5047 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -22,8 +22,6 @@ trait ReadonlyProperties { use VisibilityChecks; protected function emitProperty($result, $property) { - if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property); - $scope= $result->codegen->scope[0]; $modifiers= Modifiers::bits($property->modifiers); $scope->meta[self::PROPERTY][$property->name]= [ From fff6f8446c00ab96a5cced871e125c939e66cf14 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 5 Apr 2025 16:05:10 +0200 Subject: [PATCH 913/926] Release 9.4.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 3592cba5..c3c64db9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.4.0 / 2025-04-05 + * Merged PR #184: Add support for final properties added in PHP 8.4, see https://wiki.php.net/rfc/property-hooks; including parameter promotion, see https://wiki.php.net/rfc/final_promotion From 8e94e6f5c6ca1b2f171ffd3822d33846de817747 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 21 Apr 2025 14:09:54 +0200 Subject: [PATCH 914/926] Add PHP 8.5 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 495fe429..9c3a5c3c 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions >= '8.4' }} + continue-on-error: ${{ matrix.php-versions >= '8.5' }} strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] os: [ubuntu-latest, windows-latest] steps: From 8fc2c0af40bc14c4ae46f67cd04e9071164f10e0 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 21 Apr 2025 14:32:53 +0200 Subject: [PATCH 915/926] Add test for clone operator --- .../ast/unittest/emit/CloningTest.class.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100755 src/test/php/lang/ast/unittest/emit/CloningTest.class.php diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php new file mode 100755 index 00000000..96292fb8 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -0,0 +1,57 @@ +fixture= new class() { + public $id= 1; + + public function with($id) { + $this->id= $id; + return $this; + } + + public function __clone() { + $this->id++; + } + }; + } + + #[Test] + public function clone_operator() { + $clone= $this->run('class %T { + public function run($in) { + return clone $in; + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + + #[Test] + public function clone_function() { + $clone= $this->run('class %T { + public function run($in) { + return clone($in); + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + + #[Test] + public function clone_interceptor_called() { + $clone= $this->run('class %T { + public function run($in) { + return clone $in; + } + }', $this->fixture->with(id: 1)); + + Assert::equals([1, 2], [$this->fixture->id, $clone->id]); + } +} \ No newline at end of file From 9d210308aa0fa807a90603193525696432e9f36a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 21 Apr 2025 16:14:19 +0200 Subject: [PATCH 916/926] Fix PHP 7.4 compatibility --- src/test/php/lang/ast/unittest/emit/CloningTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php index 96292fb8..0dddf242 100755 --- a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -50,7 +50,7 @@ public function clone_interceptor_called() { public function run($in) { return clone $in; } - }', $this->fixture->with(id: 1)); + }', $this->fixture->with(1)); Assert::equals([1, 2], [$this->fixture->id, $clone->id]); } From 4357341f0cb254d8ff06bad5c05fbf576f9a6d8f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 11:22:05 +0200 Subject: [PATCH 917/926] Fix pipeline execution order --- src/main/php/lang/ast/emit/PHP.class.php | 44 +++-- .../ast/unittest/emit/PipelinesTest.class.php | 166 +++++++++++++++++- 2 files changed, 183 insertions(+), 27 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 01e3e15b..fc7256a4 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1158,42 +1158,40 @@ protected function emitNullsafeInstance($result, $instance) { $this->emitOne($result, $instance->member); } - protected function emitPipeTarget($result, $pipe, $argument) { - - // $expr |> new T(...) => new T($expr) - if ($pipe->target instanceof CallableNewExpression) { - $pipe->target->type->arguments= [$argument]; - $this->emitOne($result, $pipe->target->type); - $pipe->target->type->arguments= null; - return; - } - - // $expr |> strtoupper(...) => strtoupper($expr) - // $expr |> fn($x) => $x * 2 => (fn($x) => $x * 2)($expr) - if ($pipe->target instanceof CallableExpression) { - $this->emitOne($result, $pipe->target->expression); + protected function emitPipeTarget($result, $target, $arg) { + if ($target instanceof CallableNewExpression) { + $target->type->arguments= [new Variable(substr($arg, 1))]; + $this->emitOne($result, $target->type); + $target->type->arguments= null; + } else if ($target instanceof CallableExpression) { + $this->emitOne($result, $target->expression); + $result->out->write('('.$arg.')'); } else { $result->out->write('('); - $this->emitOne($result, $pipe->target); - $result->out->write(')'); + $this->emitOne($result, $target); + $result->out->write(')('.$arg.')'); } - - $result->out->write('('); - $this->emitOne($result, $argument); - $result->out->write(')'); } protected function emitPipe($result, $pipe) { - $this->emitPipeTarget($result, $pipe, $pipe->expression); + + // $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1] + $t= $result->temp(); + $result->out->write('['.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(','); + $this->emitPipeTarget($result, $pipe->target, $t); + $result->out->write('][1]'); } protected function emitNullsafePipe($result, $pipe) { + + // $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg) $t= $result->temp(); $result->out->write('null===('.$t.'='); $this->emitOne($result, $pipe->expression); $result->out->write(')?null:'); - - $this->emitPipeTarget($result, $pipe, new Variable(substr($t, 1))); + $this->emitPipeTarget($result, $pipe->target, $t); } protected function emitUnpack($result, $unpack) { diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index b18915a0..43bfc290 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -1,6 +1,8 @@ run('use lang\Error; class %T { + public function run() { + return "test" |> throw new Error("Test"); + } + }'); + } + + #[Test, Expect(Error::class)] + public function pipe_to_missing() { + $this->run('class %T { + public function run() { + return "test" |> "__missing"; + } + }'); + } + #[Test] public function pipe_chain() { $r= $this->run('class %T { @@ -109,7 +129,7 @@ public function run() { Assert::equals('TEST', $r); } - #[Test, Values([[['test'], 'TEST'], [[], null]])] + #[Test, Values([[['test'], 'TEST'], [[''], ''], [[], null]])] public function nullsafe_pipe($input, $expected) { $r= $this->run('class %T { public function run($arg) { @@ -120,7 +140,7 @@ public function run($arg) { Assert::equals($expected, $r); } - #[Test, Values([[null, null], ['test', 'TEST'], [' test ', 'TEST']])] + #[Test, Values([[null, null], ['', ''], ['test', 'TEST'], [' test ', 'TEST']])] public function nullsafe_chain($input, $expected) { $r= $this->run('class %T { public function run($arg) { @@ -132,7 +152,7 @@ public function run($arg) { } #[Test] - public function precedence() { + public function concat_precedence() { $r= $this->run('class %T { public function run() { return "te"."st" |> strtoupper(...); @@ -142,6 +162,54 @@ public function run() { Assert::equals('TEST', $r); } + #[Test] + public function addition_precedence() { + $r= $this->run('class %T { + public function run() { + return 5 + 2 |> fn($i) => $i * 2; + } + }'); + + Assert::equals(14, $r); + } + + #[Test] + public function comparison_precedence() { + $r= $this->run('class %T { + public function run() { + return 5 |> fn($i) => $i * 2 === 10; + } + }'); + + Assert::true($r); + } + + #[Test, Values([[0, 'even'], [1, 'odd'], [2, 'even']])] + public function ternary_precedence($arg, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return $arg |> fn($i) => $i % 2 ? "odd" : "even"; + } + }', $arg); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, 'root'], [1001, 'test'], [1002, '#unknown']])] + public function coalesce_precedence($arg, $expected) { + $r= $this->run('class %T { + private $users= [0 => "root", 1001 => "test"]; + + private function user($id) { return $this->users[$id] ?? null; } + + public function run($arg) { + return $arg |> $this->user(...) ?? "#unknown"; + } + }', $arg); + + Assert::equals($expected, $r); + } + #[Test] public function rfc_example() { $r= $this->run('class %T { @@ -156,4 +224,94 @@ public function run() { }'); Assert::equals(['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D'], array_values($r)); } + + #[Test, Expect(Error::class), Runtime(php: '>=8.5.0')] + public function rejects_by_reference_functions() { + $this->run('class %T { + private function modify(&$arg) { $arg++; } + + public function run() { + $val= 1; + return $val |> $this->modify(...); + } + }'); + } + + #[Test] + public function accepts_prefer_by_reference_functions() { + $r= $this->run('class %T { + public function run() { + return ["hello", "world"] |> array_multisort(...); + } + }'); + + Assert::true($r); + } + + #[Test] + public function execution_order() { + $r= $this->run('class %T { + public function run() { + $invoked= []; + + $first= function() use(&$invoked) { $invoked[]= "first"; return 1; }; + $second= function() use(&$invoked) { $invoked[]= "second"; return false; }; + $skipped= function() use(&$invoked) { $invoked[]= "skipped"; return $in; }; + $third= function($in) use(&$invoked) { $invoked[]= "third"; return $in; }; + $capture= function($result) use(&$invoked) { $invoked[]= $result; }; + + $first() |> ($second() ? $skipped : $third) |> $capture; + return $invoked; + } + }'); + + Assert::equals(['first', 'second', 'third', 1], $r); + } + + #[Test] + public function interrupted_by_exception() { + $r= $this->run('use lang\Error; class %T { + public function run() { + $invoked= []; + + $provide= function() use(&$invoked) { $invoked[]= "provide"; return 1; }; + $transform= function($in) use(&$invoked) { $invoked[]= "transform"; return $in * 2; }; + $throw= function() use(&$invoked) { $invoked[]= "throw"; throw new Error("Break"); }; + + try { + $provide() |> $transform |> $throw |> throw new Error("Unreachable"); + } catch (Error $e) { + $invoked[]= $e->compoundMessage(); + } + return $invoked; + } + }'); + + Assert::equals(['provide', 'transform', 'throw', 'Exception lang.Error (Break)'], $r); + } + + #[Test] + public function generators() { + $r= $this->run('class %T { + private function range($lo, $hi) { + for ($i= $lo; $i <= $hi; $i++) { + yield $i; + } + } + + private function map($fn) { + return function($it) use($fn) { + foreach ($it as $element) { + yield $fn($element); + } + }; + } + + public function run() { + return $this->range(1, 3) |> $this->map(fn($e) => $e + 1) |> iterator_to_array(...); + } + }'); + + Assert::equals([2, 3, 4], $r); + } } \ No newline at end of file From e05b5b13638323b1eba3a57e088a2b24b421f40c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 11:54:00 +0200 Subject: [PATCH 918/926] Extract pipeline emulation into trait Once php/php-src#17118 is merged, we can create a PHP 8.5 emitter which does not include it and emits pipes natively --- .../lang/ast/emit/EmulatePipelines.class.php | 48 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 30 ++---------- src/main/php/lang/ast/emit/PHP74.class.php | 1 + src/main/php/lang/ast/emit/PHP80.class.php | 1 + src/main/php/lang/ast/emit/PHP81.class.php | 1 + src/main/php/lang/ast/emit/PHP82.class.php | 1 + src/main/php/lang/ast/emit/PHP83.class.php | 2 +- src/main/php/lang/ast/emit/PHP84.class.php | 2 +- 8 files changed, 59 insertions(+), 27 deletions(-) create mode 100755 src/main/php/lang/ast/emit/EmulatePipelines.class.php diff --git a/src/main/php/lang/ast/emit/EmulatePipelines.class.php b/src/main/php/lang/ast/emit/EmulatePipelines.class.php new file mode 100755 index 00000000..d101605c --- /dev/null +++ b/src/main/php/lang/ast/emit/EmulatePipelines.class.php @@ -0,0 +1,48 @@ +type->arguments= [new Variable(substr($arg, 1))]; + $this->emitOne($result, $target->type); + $target->type->arguments= null; + } else if ($target instanceof CallableExpression) { + $this->emitOne($result, $target->expression); + $result->out->write('('.$arg.')'); + } else { + $result->out->write('('); + $this->emitOne($result, $target); + $result->out->write(')('.$arg.')'); + } + } + + protected function emitPipe($result, $pipe) { + + // $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1] + $t= $result->temp(); + $result->out->write('['.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(','); + $this->emitPipeTarget($result, $pipe->target, $t); + $result->out->write('][1]'); + } + + protected function emitNullsafePipe($result, $pipe) { + + // $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg) + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(')?null:'); + $this->emitPipeTarget($result, $pipe->target, $t); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index fc7256a4..584769e3 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1158,40 +1158,20 @@ protected function emitNullsafeInstance($result, $instance) { $this->emitOne($result, $instance->member); } - protected function emitPipeTarget($result, $target, $arg) { - if ($target instanceof CallableNewExpression) { - $target->type->arguments= [new Variable(substr($arg, 1))]; - $this->emitOne($result, $target->type); - $target->type->arguments= null; - } else if ($target instanceof CallableExpression) { - $this->emitOne($result, $target->expression); - $result->out->write('('.$arg.')'); - } else { - $result->out->write('('); - $this->emitOne($result, $target); - $result->out->write(')('.$arg.')'); - } - } - protected function emitPipe($result, $pipe) { - - // $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1] - $t= $result->temp(); - $result->out->write('['.$t.'='); $this->emitOne($result, $pipe->expression); - $result->out->write(','); - $this->emitPipeTarget($result, $pipe->target, $t); - $result->out->write('][1]'); + $result->out->write('|>'); + $this->emitOne($result, $pipe->target); } protected function emitNullsafePipe($result, $pipe) { - // $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg) + // $expr ?|> strtoupper(...) => null === ($t= $expr) ? null : $t |> strtoupper(...) $t= $result->temp(); $result->out->write('null===('.$t.'='); $this->emitOne($result, $pipe->expression); - $result->out->write(')?null:'); - $this->emitPipeTarget($result, $pipe->target, $t); + $result->out->write(')?null:'.$t.'|>'); + $this->emitOne($result, $pipe->target); } protected function emitUnpack($result, $unpack) { diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 6f97959c..d8feaec8 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -14,6 +14,7 @@ class PHP74 extends PHP { AttributesAsComments, CallablesAsClosures, ChainScopeOperators, + EmulatePipelines, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index db83e745..02c67c29 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -21,6 +21,7 @@ class PHP80 extends PHP { use ArrayUnpackUsingMerge, CallablesAsClosures, + EmulatePipelines, OmitConstantTypes, ReadonlyClasses, RewriteBlockLambdaExpressions, diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index f93b4969..f0062593 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -20,6 +20,7 @@ */ class PHP81 extends PHP { use + EmulatePipelines, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index 89fa9cbd..c8da1649 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -20,6 +20,7 @@ */ class PHP82 extends PHP { use + EmulatePipelines, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteStaticVariableInitializations, diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 7c7293b2..06879d88 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, RewriteProperties; + use EmulatePipelines, RewriteBlockLambdaExpressions, RewriteProperties; public $targetVersion= 80300; diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php index 71379f30..a9083a8f 100755 --- a/src/main/php/lang/ast/emit/PHP84.class.php +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -18,7 +18,7 @@ * @see https://wiki.php.net/rfc#php_84 */ class PHP84 extends PHP { - use RewriteBlockLambdaExpressions; + use EmulatePipelines, RewriteBlockLambdaExpressions; public $targetVersion= 80400; From 1c5583f420720d73f0ace3f411d58ac8217e36b3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 11:55:49 +0200 Subject: [PATCH 919/926] QA: Remove unused imports --- src/main/php/lang/ast/emit/PHP.class.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 584769e3..56d0be09 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -7,8 +7,6 @@ ArrayLiteral, BinaryExpression, Block, - CallableExpression, - CallableNewExpression, Comment, Expression, InstanceExpression, From 93e033fdb74940312e4b7ae16df9fa17ab204105 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 11:55:49 +0200 Subject: [PATCH 920/926] QA: Document null-safe pipe operator [skip ci] --- src/main/php/lang/ast/emit/EmulatePipelines.class.php | 5 +++-- src/main/php/lang/ast/emit/PHP.class.php | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/EmulatePipelines.class.php b/src/main/php/lang/ast/emit/EmulatePipelines.class.php index d101605c..828f3829 100755 --- a/src/main/php/lang/ast/emit/EmulatePipelines.class.php +++ b/src/main/php/lang/ast/emit/EmulatePipelines.class.php @@ -3,9 +3,10 @@ use lang\ast\nodes\{CallableExpression, CallableNewExpression, Variable}; /** - * Emulates pipelines + * Emulates pipelines / the pipe operator, including a null-safe version. * - * @see https://wiki.php.net/rfc/pipe-operator-v3#precedence + * @see https://wiki.php.net/rfc/pipe-operator-v3 + * @see https://externals.io/message/107661#107670 * @test lang.ast.unittest.emit.PipelinesTest */ trait EmulatePipelines { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 584769e3..56d0be09 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -7,8 +7,6 @@ ArrayLiteral, BinaryExpression, Block, - CallableExpression, - CallableNewExpression, Comment, Expression, InstanceExpression, From 55fc0bd12b93bbd529c1724273e396c738d58f87 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 13:14:15 +0200 Subject: [PATCH 921/926] Add more tests for operator precedence --- .../ast/unittest/emit/PipelinesTest.class.php | 27 +++++++++- .../ast/unittest/emit/TernaryTest.class.php | 53 ++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index 43bfc290..27735487 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -155,7 +155,7 @@ public function run($arg) { public function concat_precedence() { $r= $this->run('class %T { public function run() { - return "te"."st" |> strtoupper(...); + return "te" . "st" |> strtoupper(...); } }'); @@ -187,8 +187,31 @@ public function run() { #[Test, Values([[0, 'even'], [1, 'odd'], [2, 'even']])] public function ternary_precedence($arg, $expected) { $r= $this->run('class %T { + + private function odd($n) { return $n % 2; } + + public function run($arg) { + return $arg |> $this->odd(...) ? "odd" : "even"; + } + }', $arg); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, '(empty)'], [1, 'one element'], [2, '2 elements']])] + public function short_ternary_precedence($arg, $expected) { + $r= $this->run('class %T { + + private function number($n) { + return match ($n) { + 0 => null, + 1 => "one element", + default => "{$n} elements" + }; + } + public function run($arg) { - return $arg |> fn($i) => $i % 2 ? "odd" : "even"; + return $arg |> $this->number(...) ?: "(empty)"; } }', $arg); diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index 39e9eb13..b0d7b133 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -56,12 +56,61 @@ public function run($value) { #[Test, Values(eval: '[["."], [new Path(".")]]')] public function with_instanceof($value) { Assert::equals(new Path('.'), $this->run( - 'class %T { + 'use io\\Path; class %T { public function run($value) { - return $value instanceof \\io\\Path ? $value : new \\io\\Path($value); + return $value instanceof Path ? $value : new Path($value); } }', $value )); } + + #[Test, Values(['"te" . "st"', '1 + 2', '1 || 0', '2 ?? 1', '1 | 2', '4 === strlen("Test")'])] + public function precedence($lhs) { + Assert::equals('OK', $this->run( + 'class %T { + public function run() { + return '.$lhs.'? "OK" : "Error"; + } + }' + )); + } + + #[Test] + public function assignment_precedence() { + Assert::equals(['OK', 'OK'], $this->run( + 'class %T { + public function run() { + return [$a= 1 ? "OK" : "Error", $a]; + } + }' + )); + } + + #[Test] + public function yield_precedence() { + Assert::equals(['OK', null], iterator_to_array($this->run( + 'class %T { + public function run() { + yield (yield 1 ? "OK" : "Error"); + } + }' + ))); + } + + /** @see https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison.ternary */ + #[Test] + public function chaining_short_ternaries() { + Assert::equals([1, 2, 3], $this->run( + 'class %T { + public function run() { + return [ + 0 ?: 1 ?: 2 ?: 3, + 0 ?: 0 ?: 2 ?: 3, + 0 ?: 0 ?: 0 ?: 3, + ]; + } + }' + )); + } } \ No newline at end of file From a7b48e1787d68110a6db6f65f45df33e22d6fd71 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 13:21:33 +0200 Subject: [PATCH 922/926] Verify missing argument --- .../lang/ast/unittest/emit/PipelinesTest.class.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php index 27735487..6d017ef7 100755 --- a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -110,7 +110,7 @@ public function run() { } #[Test, Expect(Error::class)] - public function pipe_to_missing() { + public function missing_function() { $this->run('class %T { public function run() { return "test" |> "__missing"; @@ -118,6 +118,15 @@ public function run() { }'); } + #[Test, Expect(Error::class)] + public function missing_argument() { + $this->run('class %T { + public function run() { + return 5 |> fn($a, $b) => $a * $b; + } + }'); + } + #[Test] public function pipe_chain() { $r= $this->run('class %T { From a259b31ec458215764d40b2c75d14dcf66882c88 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 18 May 2025 13:46:48 +0200 Subject: [PATCH 923/926] QA: Add `@test` references --- src/main/php/lang/ast/emit/PHP83.class.php | 1 + src/main/php/lang/ast/emit/PHP84.class.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 06879d88..101ed9f1 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -15,6 +15,7 @@ /** * PHP 8.3 syntax * + * @test lang.ast.unittest.emit.PHP83Test * @see https://wiki.php.net/rfc#php_83 */ class PHP83 extends PHP { diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php index a9083a8f..54c0face 100755 --- a/src/main/php/lang/ast/emit/PHP84.class.php +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -15,6 +15,7 @@ /** * PHP 8.4 syntax * + * @test lang.ast.unittest.emit.PHP84Test * @see https://wiki.php.net/rfc#php_84 */ class PHP84 extends PHP { From d309fea6308aa8093070c25c7260f48f54249cae Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 28 May 2025 21:28:54 +0200 Subject: [PATCH 924/926] Use AST library release version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ad441966..4ce18abd 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.2 | ^2.15", - "xp-framework/ast": "dev-feature/pipelines as 11.6.0", + "xp-framework/ast": "^11.6", "php" : ">=7.4.0" }, "require-dev" : { From 69b90828eb5d74cb09ce26f9ef48a31d3f801246 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 28 May 2025 21:31:31 +0200 Subject: [PATCH 925/926] Document pipelines with `|>` and `?|>` --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index c3c64db9..839d7406 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,10 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #181: Add support for pipelines with `|>` and `?|>`, see + https://wiki.php.net/rfc/pipe-operator-v3 and issue #180 + (@thekid) + ## 9.4.0 / 2025-04-05 * Merged PR #184: Add support for final properties added in PHP 8.4, see From ee98e5db00c8406cb3d0e11598402cb6a330d291 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 28 May 2025 21:38:49 +0200 Subject: [PATCH 926/926] Release 9.5.0 --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 839d7406..7f45c1e7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +## 9.5.0 / 2025-05-28 + * Merged PR #181: Add support for pipelines with `|>` and `?|>`, see https://wiki.php.net/rfc/pipe-operator-v3 and issue #180 (@thekid)