From 34d589cce3b5c472f3161a864999aa878550aec3 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 May 2026 16:44:21 +0200 Subject: [PATCH 1/4] Refactor function scopes: Capturing and returning --- src/main/php/lang/ast/syntax/PHP.class.php | 95 +++++++++++++------ .../ast/unittest/parse/ClosuresTest.class.php | 9 ++ .../unittest/parse/FunctionsTest.class.php | 9 ++ .../ast/unittest/parse/LambdasTest.class.php | 28 ++++++ .../parse/MatchExpressionTest.class.php | 17 +++- .../ast/unittest/parse/MembersTest.class.php | 9 ++ 6 files changed, 138 insertions(+), 29 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 35346ca4..e9f82de7 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -307,12 +307,6 @@ public function __construct() { return new CloneExpression($arguments, $token->line); }); - $this->prefix('{', 0, function($parse, $token) { - $statements= $this->statements($parse); - $parse->expecting('}', 'block'); - return new Block($statements, $token->line); - }); - $this->prefix('[', 0, function($parse, $token) { return new ArrayLiteral($this->list($parse, ']', 'array literal'), $token->line); }); @@ -400,9 +394,8 @@ public function __construct() { $this->prefix('fn', 0, function($parse, $token) { $signature= $this->signature($parse); - $parse->expecting('=>', 'fn'); - - return new LambdaExpression($signature, $this->expression($parse, 0), false, $token->line); + $scope= $this->scope($parse, 'fn'); + return new LambdaExpression($signature, $scope, false, $token->line); }); $this->prefix('function', 0, function($parse, $token) { @@ -435,8 +428,8 @@ public function __construct() { } else if ('fn' === $parse->token->value) { $parse->forward(); $signature= $this->signature($parse); - $parse->expecting('=>', 'fn'); - return new LambdaExpression($signature, $this->expression($parse, 0), true, $token->line); + $scope= $this->scope($parse, 'fn'); + return new LambdaExpression($signature, $scope, true, $token->line); } else { return new Literal($token->value, $token->line); } @@ -463,15 +456,14 @@ public function __construct() { while ('}' !== $parse->token->value) { if ('default' === $parse->token->value) { $parse->forward(); - $parse->expecting('=>', 'match'); - $default= $this->expression($parse, 0); + $default= $this->scope($parse, 'match'); } else { $match= []; do { $match[]= $this->expression($parse, 0); } while (',' === $parse->token->value && $parse->forward() | true); - $parse->expecting('=>', 'match'); - $cases[]= new MatchCondition($match, $this->expression($parse, 0), $parse->token->line); + + $cases[]= new MatchCondition($match, $this->scope($parse, 'match'), $parse->token->line); } if (',' === $parse->token->value) { @@ -533,6 +525,12 @@ public function __construct() { return new Literal($token->value, $token->line); }); + // Blocks and standalone semicolons + $this->stmt('{', function($parse, $token) { + $statements= $this->statements($parse); + $parse->expecting('}', 'block'); + return new Block($statements, $token->line); + }); $this->stmt(';', function($parse, $token) { return null; }); // Unexpected standalone symbols, warn but continue @@ -886,14 +884,10 @@ public function __construct() { // Function expression used as statement (e.g. for pure side-effects!) if ('(' === $parse->token->value) { - $parse->queue= [$parse->token]; + array_unshift($parse->queue, $parse->token); $parse->token= new Token($this->symbol('function')); $parse->token->line= $token->line; - $expr= $this->expression($parse, 0); - - $parse->queue= [$parse->token]; - $parse->token= new Token($this->symbol(';')); - return $expr; + return $this->expression($parse, 0); } if ('&' === $parse->token->value) { @@ -907,9 +901,19 @@ public function __construct() { $parse->forward(); $signature= $this->signature($parse, $byref); - $parse->expecting('{', 'function'); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); + + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'function'); + } else if ('=>' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + $statements= [new ReturnStatement($expr, $expr->line)]; + $parse->expecting(';', 'function'); + } else { + $parse->expecting('=> or { ... }', 'function'); + } return new FunctionDeclaration($name, $signature, $statements, $token->line); }); @@ -1142,6 +1146,11 @@ public function __construct() { $parse->forward(); $statements= $this->statements($parse); $parse->expecting('}', 'method declaration'); + } else if ('=>' === $parse->token->value) { // Single-expression method + $parse->forward(); + $expr= $this->expression($parse, 0); + $statements= [new ReturnStatement($expr, $expr->line)]; + $parse->expecting(';', 'method declaration'); } else if (';' === $parse->token->value) { // Abstract or interface method $statements= null; $parse->expecting(';', 'method declaration'); @@ -1591,6 +1600,29 @@ private function parameters($parse) { return $parameters; } + public function scope($parse, $context) { + if ('=>' === $parse->token->value) { + $parse->forward(); + + // BC: Support deprecated arrow block syntax + if ('{' === $parse->token->value) { + trigger_error('Arrow block syntax `=> { ... }`', E_USER_DEPRECATED); + goto block; + } + + return $this->expression($parse, 0); + } else if ('{' === $parse->token->value) { + block: $line= $parse->token->line; + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', $context); + return new Block($statements, $line); + } else { + $parse->expecting('=> or { ... }', $context); + return null; + } + } + public function body($id, $func) { $this->body[$id]= $func->bindTo($this, static::class); } @@ -1689,9 +1721,18 @@ public function closure($parse, $static) { $return= null; } - $parse->expecting('{', 'function'); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); + if ('{' === $parse->token->value) { + $parse->forward(); + $statements= $this->statements($parse); + $parse->expecting('}', 'function'); + } else if ('=>' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + $statements= [new ReturnStatement($expr, $expr->line)]; + $parse->expecting(';', 'function'); + } else { + $parse->expecting('=> or { ... }', 'function'); + } return new ClosureExpression(new Signature($parameters, $return, false, $line), $use, $statements, $static, $line); } diff --git a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php index 25d40afa..f4e92e86 100755 --- a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php @@ -47,6 +47,15 @@ public function with_body() { ); } + #[Test] + public function single_expression() { + $scope= [new ReturnStatement(new Literal('true', self::LINE), self::LINE)]; + $this->assertParsed( + [new ClosureExpression(new Signature([], null, false, self::LINE), null, $scope, false, self::LINE)], + 'function() => true;' + ); + } + #[Test] public function with_param() { $params= [new Parameter('a', null, null, false, false, null, null, null, self::LINE)]; 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 40c98c7b..e05466d5 100755 --- a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php @@ -41,6 +41,15 @@ public function empty_function_without_parameters() { ); } + #[Test] + public function single_expression_function() { + $scope= [new ReturnStatement(new Literal('"A"', self::LINE), self::LINE)]; + $this->assertParsed( + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), $scope, self::LINE)], + 'function a() => "A";' + ); + } + #[Test] public function returning_by_reference() { $this->assertParsed( 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 ad6ce41b..9c410ed9 100755 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php @@ -42,6 +42,33 @@ public function short_closure_as_arg() { #[Test] public function short_closure_with_block() { + $this->assertParsed( + [new LambdaExpression( + new Signature([$this->parameter], null, false, self::LINE), + new Block([new ReturnStatement($this->expression, self::LINE)], self::LINE), + false, + self::LINE + )], + 'fn($a) { return $a + 1; };' + ); + } + + #[Test] + public function static_short_closure_with_block() { + $this->assertParsed( + [new LambdaExpression( + new Signature([$this->parameter], null, false, self::LINE), + new Block([new ReturnStatement($this->expression, self::LINE)], self::LINE), + true, + self::LINE + )], + 'static fn($a) { return $a + 1; };' + ); + } + + /** @deprecated */ + #[Test] + public function short_closure_with_arrow_and_block() { $this->assertParsed( [new LambdaExpression( new Signature([$this->parameter], null, false, self::LINE), @@ -51,5 +78,6 @@ public function short_closure_with_block() { )], 'fn($a) => { return $a + 1; };' ); + \xp::gc(); // Swallow deprecation warning } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MatchExpressionTest.class.php b/src/test/php/lang/ast/unittest/parse/MatchExpressionTest.class.php index e6e6b0c7..006b8ac0 100755 --- a/src/test/php/lang/ast/unittest/parse/MatchExpressionTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MatchExpressionTest.class.php @@ -36,11 +36,24 @@ public function match() { } #[Test] - public function match_with_block() { + public function match_with_default_block() { $default= new Block([new ReturnStatement(new Literal('false', self::LINE), self::LINE)], self::LINE); $this->assertParsed( [new MatchExpression(new Variable('arg', self::LINE), [], $default, self::LINE)], - 'match ($arg) { default => { return false; } };' + 'match ($arg) { default { return false; } };' + ); + } + + #[Test] + public function match_with_case_block() { + $cases= [new MatchCondition( + [new Literal('0', self::LINE)], + new Block([new ReturnStatement(new Literal('false', self::LINE), self::LINE)], self::LINE), + self::LINE + )]; + $this->assertParsed( + [new MatchExpression(new Variable('arg', self::LINE), $cases, null, self::LINE)], + 'match ($arg) { 0 { return false; } };' ); } 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 9320a31c..7041b73d 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -87,6 +87,15 @@ public function method_returning_reference() { $this->assertParsed([$class], 'class A { private static function &a() { } }'); } + #[Test] + public function single_expression_method() { + $scope= [new ReturnStatement(new Literal('"A"', self::LINE), self::LINE)]; + $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); + $class->declare(new Method(['private'], 'a', new Signature([], null, false, self::LINE), $scope, null, null, self::LINE)); + + $this->assertParsed([$class], 'class A { private function a() => "A"; }'); + } + #[Test] public function class_constant() { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); From db04895b1751f2f157a2187b2fc1e9338358d170 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 May 2026 18:29:26 +0200 Subject: [PATCH 2/4] Make methods', functions', closures' and lambdas' bodies consistent --- .../ast/nodes/ClosureExpression.class.php | 4 +- .../ast/nodes/FunctionDeclaration.class.php | 4 +- .../lang/ast/nodes/LambdaExpression.class.php | 2 +- src/main/php/lang/ast/nodes/Method.class.php | 10 +++- src/main/php/lang/ast/syntax/PHP.class.php | 50 +++++----------- .../ast/unittest/parse/BracedTest.class.php | 3 +- .../ast/unittest/parse/ClosuresTest.class.php | 45 ++++++++------- .../ast/unittest/parse/CommentTest.class.php | 11 +++- .../unittest/parse/FunctionsTest.class.php | 57 +++++++++++-------- .../ast/unittest/parse/MembersTest.class.php | 26 +++++---- 10 files changed, 114 insertions(+), 98 deletions(-) diff --git a/src/main/php/lang/ast/nodes/ClosureExpression.class.php b/src/main/php/lang/ast/nodes/ClosureExpression.class.php index 674b0642..6c3916cd 100755 --- a/src/main/php/lang/ast/nodes/ClosureExpression.class.php +++ b/src/main/php/lang/ast/nodes/ClosureExpression.class.php @@ -13,5 +13,7 @@ public function __construct($signature, $use, $body, $static= false, $line= -1) } /** @return iterable */ - public function children() { return $this->body; } + public function children() { + return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php b/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php index 0b57d6c5..8777a1f9 100755 --- a/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php +++ b/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php @@ -12,5 +12,7 @@ public function __construct($name, $signature, $body, $line= -1) { } /** @return iterable */ - public function children() { return $this->body; } + public function children() { + return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/LambdaExpression.class.php b/src/main/php/lang/ast/nodes/LambdaExpression.class.php index 61cfe188..469cc69f 100755 --- a/src/main/php/lang/ast/nodes/LambdaExpression.class.php +++ b/src/main/php/lang/ast/nodes/LambdaExpression.class.php @@ -13,6 +13,6 @@ public function __construct($signature, $body, $static= false, $line= -1) { /** @return iterable */ public function children() { - return is_array($this->body) ? $this->body : [&$this->body]; + return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/Method.class.php b/src/main/php/lang/ast/nodes/Method.class.php index 6dad7941..ca47e9ff 100755 --- a/src/main/php/lang/ast/nodes/Method.class.php +++ b/src/main/php/lang/ast/nodes/Method.class.php @@ -46,5 +46,13 @@ public function append($node) { } /** @return iterable */ - public function children() { return (array)$this->body; } + public function children() { + if (null === $this->body) { + return []; + } else if (is_array($this->body)) { // BC + return $this->body; + } else { + return [&$this->body]; + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index e9f82de7..2b3a932a 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -901,21 +901,9 @@ public function __construct() { $parse->forward(); $signature= $this->signature($parse, $byref); + $scope= $this->scope($parse, 'function'); - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); - } else if ('=>' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - $statements= [new ReturnStatement($expr, $expr->line)]; - $parse->expecting(';', 'function'); - } else { - $parse->expecting('=> or { ... }', 'function'); - } - - return new FunctionDeclaration($name, $signature, $statements, $token->line); + return new FunctionDeclaration($name, $signature, $scope, $token->line); }); $this->stmt('class', function($parse, $token) { @@ -1142,20 +1130,20 @@ public function __construct() { $parse->forward(); $signature= $this->signature($parse, $byref); - if ('{' === $parse->token->value) { // Regular body + // Cannot use scope() here as we require a semicolon after single-expressions + if ('{' === $parse->token->value) { $parse->forward(); - $statements= $this->statements($parse); + $scope= new Block($this->statements($parse), $parse->token->line); $parse->expecting('}', 'method declaration'); - } else if ('=>' === $parse->token->value) { // Single-expression method + } else if ('=>' === $parse->token->value) { $parse->forward(); - $expr= $this->expression($parse, 0); - $statements= [new ReturnStatement($expr, $expr->line)]; + $scope= $this->expression($parse, 0); $parse->expecting(';', 'method declaration'); - } else if (';' === $parse->token->value) { // Abstract or interface method - $statements= null; + } else if (';' === $parse->token->value) { + $scope= null; $parse->expecting(';', 'method declaration'); } else { - $parse->expecting('{ or ;', 'method declaration'); + $parse->expecting('{, => or ;', 'method declaration'); return; } @@ -1163,7 +1151,7 @@ public function __construct() { $modifiers, $name, $signature, - $statements, + $scope, $meta[DETAIL_ANNOTATIONS] ?? null, $comment, $line @@ -1721,20 +1709,8 @@ public function closure($parse, $static) { $return= null; } - if ('{' === $parse->token->value) { - $parse->forward(); - $statements= $this->statements($parse); - $parse->expecting('}', 'function'); - } else if ('=>' === $parse->token->value) { - $parse->forward(); - $expr= $this->expression($parse, 0); - $statements= [new ReturnStatement($expr, $expr->line)]; - $parse->expecting(';', 'function'); - } else { - $parse->expecting('=> or { ... }', 'function'); - } - - return new ClosureExpression(new Signature($parameters, $return, false, $line), $use, $statements, $static, $line); + $scope= $this->scope($parse, 'function'); + return new ClosureExpression(new Signature($parameters, $return, false, $line), $use, $scope, $static, $line); } public function block($parse) { diff --git a/src/test/php/lang/ast/unittest/parse/BracedTest.class.php b/src/test/php/lang/ast/unittest/parse/BracedTest.class.php index 6fd10f5c..aee4249d 100755 --- a/src/test/php/lang/ast/unittest/parse/BracedTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/BracedTest.class.php @@ -1,6 +1,7 @@ assertParsed( - [new Braced(new ClosureExpression($signature, null, [], false, self::LINE), self::LINE)], + [new Braced(new ClosureExpression($signature, null, new Block([], self::LINE), false, self::LINE), self::LINE)], '(function(T &$arg) { });' ); } diff --git a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php index f4e92e86..10049767 100755 --- a/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/ClosuresTest.class.php @@ -2,6 +2,7 @@ use lang\ast\nodes\{ BinaryExpression, + Block, Braced, ClosureExpression, InvokeExpression, @@ -19,22 +20,28 @@ class ClosuresTest extends ParseTest { #[Before] public function returns() { - $this->returns= new ReturnStatement( - new BinaryExpression( - new Variable('a', self::LINE), - '+', - new Literal('1', self::LINE), + $this->returns= new Block( + [new ReturnStatement( + new BinaryExpression( + new Variable('a', self::LINE), + '+', + new Literal('1', self::LINE), + self::LINE + ), self::LINE - ), + )], self::LINE ); } #[Before] public function invoke() { - $this->invoke= new InvokeExpression( - new Literal('var_dump', self::LINE), - [new Literal('true', self::LINE)], + $this->invoke= new Block( + [new InvokeExpression( + new Literal('var_dump', self::LINE), + [new Literal('true', self::LINE)], + self::LINE + )], self::LINE ); } @@ -42,14 +49,14 @@ public function invoke() { #[Test] public function with_body() { $this->assertParsed( - [new ClosureExpression(new Signature([], null, false, self::LINE), null, [$this->returns], false, self::LINE)], + [new ClosureExpression(new Signature([], null, false, self::LINE), null, $this->returns, false, self::LINE)], 'function() { return $a + 1; };' ); } #[Test] public function single_expression() { - $scope= [new ReturnStatement(new Literal('true', self::LINE), self::LINE)]; + $scope= new Literal('true', self::LINE); $this->assertParsed( [new ClosureExpression(new Signature([], null, false, self::LINE), null, $scope, false, self::LINE)], 'function() => true;' @@ -60,7 +67,7 @@ public function single_expression() { public function with_param() { $params= [new Parameter('a', null, null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new ClosureExpression(new Signature($params, null, false, self::LINE), null, [$this->returns], false, self::LINE)], + [new ClosureExpression(new Signature($params, null, false, self::LINE), null, $this->returns, false, self::LINE)], 'function($a) { return $a + 1; };' ); } @@ -68,7 +75,7 @@ public function with_param() { #[Test] public function with_use_by_value() { $this->assertParsed( - [new ClosureExpression(new Signature([], null, false, self::LINE), ['$a', '$b'], [$this->returns], false, self::LINE)], + [new ClosureExpression(new Signature([], null, false, self::LINE), ['$a', '$b'], $this->returns, false, self::LINE)], 'function() use($a, $b) { return $a + 1; };' ); } @@ -76,7 +83,7 @@ public function with_use_by_value() { #[Test] public function with_use_and_return() { $this->assertParsed( - [new ClosureExpression(new Signature([], new Type('int'), false, self::LINE), ['$a'], [$this->returns], false, self::LINE)], + [new ClosureExpression(new Signature([], new Type('int'), false, self::LINE), ['$a'], $this->returns, false, self::LINE)], 'function() use($a): int { return $a + 1; };' ); } @@ -84,7 +91,7 @@ public function with_use_and_return() { #[Test] public function with_use_by_reference() { $this->assertParsed( - [new ClosureExpression(new Signature([], null, false, self::LINE), ['$a', '&$b'], [$this->returns], false, self::LINE)], + [new ClosureExpression(new Signature([], null, false, self::LINE), ['$a', '&$b'], $this->returns, false, self::LINE)], 'function() use($a, &$b) { return $a + 1; };' ); } @@ -92,7 +99,7 @@ public function with_use_by_reference() { #[Test] public function with_return_type() { $this->assertParsed( - [new ClosureExpression(new Signature([], new Type('int'), false, self::LINE), null, [$this->invoke], false, self::LINE)], + [new ClosureExpression(new Signature([], new Type('int'), false, self::LINE), null, $this->invoke, false, self::LINE)], 'function(): int { var_dump(true); };' ); } @@ -100,7 +107,7 @@ public function with_return_type() { #[Test] public function with_nullable_return_type() { $this->assertParsed( - [new ClosureExpression(new Signature([], new Type('?int'), false, self::LINE), null, [$this->invoke], false, self::LINE)], + [new ClosureExpression(new Signature([], new Type('?int'), false, self::LINE), null, $this->invoke, false, self::LINE)], 'function(): ?int { var_dump(true); };' ); } @@ -108,7 +115,7 @@ public function with_nullable_return_type() { #[Test] public function static_function() { $this->assertParsed( - [new ClosureExpression(new Signature([], null, false, self::LINE), null, [$this->invoke], true, self::LINE)], + [new ClosureExpression(new Signature([], null, false, self::LINE), null, $this->invoke, true, self::LINE)], 'static function() { var_dump(true); };' ); } @@ -119,7 +126,7 @@ public function iife_with_statement() { $this->assertParsed( [new InvokeExpression( new Braced( - new ClosureExpression(new Signature([], null, false, self::LINE), null, [$this->invoke], false, self::LINE), + new ClosureExpression(new Signature([], null, false, self::LINE), null, $this->invoke, false, self::LINE), self::LINE ), [], 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 4f3fbbb8..d6d34f2b 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,6 @@ declare(new Method(['public'], '__construct', new Signature([], null, false, 4), [], null, new Comment('/** @api */', 3), 4)); + $class->declare(new Method( + ['public'], + '__construct', + new Signature([], null, false, 4), + new Block([], 4), + null, + new Comment('/** @api */', 3), 4) + ); $this->assertParsed([$class], ' class T { 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 e05466d5..116ed520 100755 --- a/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/FunctionsTest.class.php @@ -4,6 +4,7 @@ ArrayLiteral, Assignment, BinaryExpression, + Block, Braced, FunctionDeclaration, Literal, @@ -15,9 +16,10 @@ YieldFromExpression }; use lang\ast\types\{IsFunction, IsLiteral, IsNullable, IsUnion, IsValue}; -use test\{Assert, Test, Values}; +use test\{Assert, Before, Test, Values}; class FunctionsTest extends ParseTest { + private $empty; /** @return iterable */ private function types() { @@ -33,17 +35,22 @@ private function types() { yield ['(function(): string)', new IsFunction([], new IsLiteral('string'))]; } + #[Before] + public function empty() { + $this->empty= new Block([], self::LINE); + } + #[Test] public function empty_function_without_parameters() { $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), $this->empty, self::LINE)], 'function a() { }' ); } #[Test] public function single_expression_function() { - $scope= [new ReturnStatement(new Literal('"A"', self::LINE), self::LINE)]; + $scope= new Literal('"A"', self::LINE); $this->assertParsed( [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), $scope, self::LINE)], 'function a() => "A";' @@ -53,7 +60,7 @@ public function single_expression_function() { #[Test] public function returning_by_reference() { $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, true, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, true, self::LINE), $this->empty, self::LINE)], 'function &a() { }' ); } @@ -62,8 +69,8 @@ public function returning_by_reference() { public function two_functions() { $this->assertParsed( [ - new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [], self::LINE), - new FunctionDeclaration('b', new Signature([], null, false, self::LINE), [], self::LINE) + new FunctionDeclaration('a', new Signature([], null, false, self::LINE), $this->empty, self::LINE), + new FunctionDeclaration('b', new Signature([], null, false, self::LINE), $this->empty, self::LINE) ], 'function a() { } function b() { }' ); @@ -73,7 +80,7 @@ public function two_functions() { public function with_parameter($name) { $params= [new Parameter($name, null, null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a($'.$name.') { }' ); } @@ -82,7 +89,7 @@ public function with_parameter($name) { public function with_reference_parameter() { $params= [new Parameter('param', null, null, true, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a(&$param) { }' ); } @@ -91,7 +98,7 @@ public function with_reference_parameter() { public function dangling_comma_in_parameter_lists() { $params= [new Parameter('param', null, null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a($param, ) { }' ); } @@ -100,7 +107,7 @@ public function dangling_comma_in_parameter_lists() { public function with_typed_parameter($declaration, $expected) { $params= [new Parameter('param', $expected, null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a('.$declaration.' $param) { }' ); } @@ -109,7 +116,7 @@ public function with_typed_parameter($declaration, $expected) { public function with_nullable_typed_parameter() { $params= [new Parameter('param', new IsNullable(new IsLiteral('string')), null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a(?string $param) { }' ); } @@ -118,7 +125,7 @@ public function with_nullable_typed_parameter() { public function with_variadic_parameter() { $params= [new Parameter('param', null, null, false, true, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a(... $param) { }' ); } @@ -127,7 +134,7 @@ public function with_variadic_parameter() { public function with_optional_parameter() { $params= [new Parameter('param', null, new Literal('null', self::LINE), false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a($param= null) { }' ); } @@ -136,7 +143,7 @@ public function with_optional_parameter() { public function with_parameter_named_function() { $params= [new Parameter('function', null, null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a($function, ) { }' ); } @@ -145,7 +152,7 @@ public function with_parameter_named_function() { public function with_typed_parameter_named_function() { $params= [new Parameter('function', new IsFunction([], new IsLiteral('void')), null, false, false, null, null, null, self::LINE)]; $this->assertParsed( - [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature($params, null, false, self::LINE), $this->empty, self::LINE)], 'function a((function(): void) $function) { }' ); } @@ -153,7 +160,7 @@ public function with_typed_parameter_named_function() { #[Test, Values(from: 'types')] public function with_return_type($declaration, $expected) { $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], $expected, false, self::LINE), [], self::LINE)], + [new FunctionDeclaration('a', new Signature([], $expected, false, self::LINE), $this->empty, self::LINE)], 'function a(): '.$declaration.' { }' ); } @@ -162,7 +169,7 @@ public function with_return_type($declaration, $expected) { public function generator() { $yield= new YieldExpression(null, null, self::LINE); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { yield; }' ); } @@ -171,7 +178,7 @@ public function generator() { public function generator_with_value() { $yield= new YieldExpression(null, new Literal('1', self::LINE), self::LINE); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { yield 1; }' ); } @@ -180,7 +187,7 @@ public function generator_with_value() { 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, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { yield "number" => 1; }' ); } @@ -189,7 +196,7 @@ public function generator_with_key_and_value() { public function generator_delegation() { $yield= new YieldFromExpression(new ArrayLiteral([], self::LINE), self::LINE); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { yield from []; }' ); } @@ -203,7 +210,7 @@ public function assign_to_yield() { self::LINE ); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { $value= yield; }' ); } @@ -217,7 +224,7 @@ public function assign_to_yield_with_braced() { self::LINE ); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { $value= yield (1); }' ); } @@ -232,7 +239,7 @@ public function assign_to_yield_in_braces() { self::LINE ); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], 'function a() { $value= (yield); }' ); } @@ -246,7 +253,7 @@ public function assign_to_yield_in_array($declaration) { self::LINE ); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], $declaration ); } @@ -260,7 +267,7 @@ public function assign_to_yield_in_map($declaration) { self::LINE ); $this->assertParsed( - [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), [$yield], self::LINE)], + [new FunctionDeclaration('a', new Signature([], null, false, self::LINE), new Block([$yield], self::LINE), self::LINE)], $declaration ); } 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 7041b73d..172374bd 100755 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php @@ -20,9 +20,10 @@ Parameter }; use lang\ast\types\{IsFunction, IsLiteral, IsNullable, IsUnion, IsValue, IsGeneric}; -use test\{Assert, Test, Values}; +use test\{Assert, Before, Test, Values}; class MembersTest extends ParseTest { + private $empty; /** @return iterable */ private function types() { @@ -38,6 +39,11 @@ private function types() { yield ['(function(): string)', new IsFunction([], new IsLiteral('string'))]; } + #[Before] + public function empty() { + $this->empty= new Block([], self::LINE); + } + #[Test] public function private_instance_property() { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); @@ -66,7 +72,7 @@ public function private_instance_properties() { #[Test] public function private_instance_method() { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['private'], 'a', new Signature([], null, false, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['private'], 'a', new Signature([], null, false, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { private function a() { } }'); } @@ -74,7 +80,7 @@ public function private_instance_method() { #[Test] public function private_static_method() { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['private', 'static'], 'a', new Signature([], null, false, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['private', 'static'], 'a', new Signature([], null, false, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { private static function a() { } }'); } @@ -82,14 +88,14 @@ public function private_static_method() { #[Test] public function method_returning_reference() { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['private', 'static'], 'a', new Signature([], null, true, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['private', 'static'], 'a', new Signature([], null, true, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { private static function &a() { } }'); } #[Test] public function single_expression_method() { - $scope= [new ReturnStatement(new Literal('"A"', self::LINE), self::LINE)]; + $scope= new Literal('"A"', self::LINE); $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); $class->declare(new Method(['private'], 'a', new Signature([], null, false, self::LINE), $scope, null, null, self::LINE)); @@ -125,7 +131,7 @@ public function private_class_constant() { public function method_with_typed_parameter($declaration, $expected) { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); $params= [new Parameter('param', $expected, null, false, false, null, null, null, self::LINE)]; - $class->declare(new Method(['public'], 'a', new Signature($params, null, false, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['public'], 'a', new Signature($params, null, false, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { public function a('.$declaration.' $param) { } }'); } @@ -133,7 +139,7 @@ public function method_with_typed_parameter($declaration, $expected) { #[Test, Values(from: 'types')] public function method_with_return_type($declaration, $expected) { $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['public'], 'a', new Signature([], $expected, false, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['public'], 'a', new Signature([], $expected, false, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { public function a(): '.$declaration.' { } }'); } @@ -142,7 +148,7 @@ public function method_with_return_type($declaration, $expected) { public function method_with_annotation() { $annotations= new Annotations(['Test' => []], self::LINE); $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['public'], 'a', new Signature([], null, false, self::LINE), [], $annotations, null, self::LINE)); + $class->declare(new Method(['public'], 'a', new Signature([], null, false, self::LINE), $this->empty, $annotations, null, self::LINE)); $this->assertParsed([$class], 'class A { #[Test] public function a() { } }'); } @@ -151,7 +157,7 @@ public function method_with_annotation() { public function method_with_annotations() { $annotations= new Annotations(['Test' => [], 'Ignore' => [new Literal('"Not implemented"', self::LINE)]], self::LINE); $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['public'], 'a', new Signature([], null, false, self::LINE), [], $annotations, null, self::LINE)); + $class->declare(new Method(['public'], 'a', new Signature([], null, false, self::LINE), $this->empty, $annotations, null, self::LINE)); $this->assertParsed([$class], 'class A { #[Test, Ignore("Not implemented")] public function a() { } }'); } @@ -410,7 +416,7 @@ public function asymmetric_property() { public function asymmetric_property_as_constructor_argument() { $params= [new Parameter('a', new IsLiteral('int'), null, false, false, ['private(set)'], null, null, self::LINE)]; $class= new ClassDeclaration([], new IsValue('\\A'), null, [], [], null, null, self::LINE); - $class->declare(new Method(['public'], '__construct', new Signature($params, null, false, self::LINE), [], null, null, self::LINE)); + $class->declare(new Method(['public'], '__construct', new Signature($params, null, false, self::LINE), $this->empty, null, null, self::LINE)); $this->assertParsed([$class], 'class A { public function __construct(private(set) int $a) { } }'); } From 6582a51c5de847b3a725b10e4937ad13efaabcbd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 May 2026 19:33:08 +0200 Subject: [PATCH 3/4] Move BC handling of arrays to constructor --- .../php/lang/ast/nodes/ClosureExpression.class.php | 6 ++---- .../php/lang/ast/nodes/FunctionDeclaration.class.php | 6 ++---- .../php/lang/ast/nodes/LambdaExpression.class.php | 6 ++---- src/main/php/lang/ast/nodes/Method.class.php | 12 ++---------- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/main/php/lang/ast/nodes/ClosureExpression.class.php b/src/main/php/lang/ast/nodes/ClosureExpression.class.php index 6c3916cd..ebbff8aa 100755 --- a/src/main/php/lang/ast/nodes/ClosureExpression.class.php +++ b/src/main/php/lang/ast/nodes/ClosureExpression.class.php @@ -7,13 +7,11 @@ class ClosureExpression extends Annotated { public function __construct($signature, $use, $body, $static= false, $line= -1) { $this->signature= $signature; $this->use= $use; - $this->body= $body; + $this->body= is_array($body) ? new Block($body, $line) : $body; $this->static= $static; $this->line= $line; } /** @return iterable */ - public function children() { - return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC - } + public function children() { return [&$this->body]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php b/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php index 8777a1f9..14529f27 100755 --- a/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php +++ b/src/main/php/lang/ast/nodes/FunctionDeclaration.class.php @@ -7,12 +7,10 @@ class FunctionDeclaration extends Annotated { public function __construct($name, $signature, $body, $line= -1) { $this->name= $name; $this->signature= $signature; - $this->body= $body; + $this->body= is_array($body) ? new Block($body, $line) : $body; $this->line= $line; } /** @return iterable */ - public function children() { - return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC - } + public function children() { return [&$this->body]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/LambdaExpression.class.php b/src/main/php/lang/ast/nodes/LambdaExpression.class.php index 469cc69f..4c283244 100755 --- a/src/main/php/lang/ast/nodes/LambdaExpression.class.php +++ b/src/main/php/lang/ast/nodes/LambdaExpression.class.php @@ -6,13 +6,11 @@ class LambdaExpression extends Annotated { public function __construct($signature, $body, $static= false, $line= -1) { $this->signature= $signature; - $this->body= $body; + $this->body= is_array($body) ? new Block($body, $line) : $body; $this->static= $static; $this->line= $line; } /** @return iterable */ - public function children() { - return is_array($this->body) ? $this->body : [&$this->body]; // Array = BC - } + public function children() { return [&$this->body]; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/Method.class.php b/src/main/php/lang/ast/nodes/Method.class.php index ca47e9ff..253e77c6 100755 --- a/src/main/php/lang/ast/nodes/Method.class.php +++ b/src/main/php/lang/ast/nodes/Method.class.php @@ -8,7 +8,7 @@ public function __construct($modifiers, $name, $signature, $body= null, $annotat $this->name= $name; $this->modifiers= $modifiers; $this->signature= $signature; - $this->body= $body; + $this->body= is_array($body) ? new Block($body, $line) : $body; parent::__construct($annotations, $comment, $line); } @@ -46,13 +46,5 @@ public function append($node) { } /** @return iterable */ - public function children() { - if (null === $this->body) { - return []; - } else if (is_array($this->body)) { // BC - return $this->body; - } else { - return [&$this->body]; - } - } + public function children() { return null === $this->body ? [] : [&$this->body]; } } \ No newline at end of file From 6bd5c78750c3bfec6b605e96d8a0ece07210fc08 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 23 May 2026 19:53:48 +0200 Subject: [PATCH 4/4] Make blocks iterable --- src/main/php/lang/ast/nodes/Block.class.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/nodes/Block.class.php b/src/main/php/lang/ast/nodes/Block.class.php index 440eef3f..fae0bf49 100755 --- a/src/main/php/lang/ast/nodes/Block.class.php +++ b/src/main/php/lang/ast/nodes/Block.class.php @@ -1,8 +1,9 @@ statements; } + + /** Iteration support */ + public function getIterator(): Traversable { yield from $this->statements; } } \ No newline at end of file