From 79bf2d4002e97f5e4dddc7238d8e8f21ce4e315e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 16:01:32 +0200 Subject: [PATCH 1/7] Add syntactic support for primary constructors See https://wiki.php.net/rfc/primary-constructors --- src/main/php/lang/ast/syntax/PHP.class.php | 22 +++++++++++++++++++++- 1 file changed, 21 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 a318d19..e26590b 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -911,7 +911,27 @@ public function __construct() { $parse->comment= null; $name= $this->type($parse, false); - return $this->class($parse, $name, $comment); + // Primary constructors + if ('(' === $parse->token->value) { + $parse->expecting('(', 'class'); + $parameters= $this->parameters($parse); + $parse->expecting(')', 'class'); + + $class= $this->class($parse, $name, $comment); + $class->declare(new Method( + ['public'], + '__construct', + new Signature($parameters, null, false, $token->line), + new Block([], $token->line), + null, + $comment, + $token->line, + )); + } else { + $class= $this->class($parse, $name, $comment); + } + + return $class; }); $this->stmt('interface', function($parse, $token) { From 22049de04006bf85276da292c775bfdd390adfbd Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 18:04:51 +0200 Subject: [PATCH 2/7] Use line number where class is declared --- src/main/php/lang/ast/syntax/PHP.class.php | 15 +++++++-------- 1 file changed, 7 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 4e6f996..5d53541 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -350,7 +350,7 @@ public function __construct() { // callable reference vs. `new T(...$it)` - a call with an unpacked argument if ($callable) { if (null === $type) { - $class= $this->class($parse, null); + $class= $this->class($parse, null, null, [], $parse->token->line); $class->annotations= $annotations; $new= new NewClassExpression($class, null, $token->line); } else { @@ -361,7 +361,7 @@ public function __construct() { } if (null === $type) { - $class= $this->class($parse, null); + $class= $this->class($parse, null, null, [], $parse->token->line); $class->annotations= $annotations; return new NewClassExpression($class, $arguments, $token->line); } else { @@ -917,8 +917,8 @@ public function __construct() { $parameters= $this->parameters($parse); $parse->expecting(')', 'class'); - $class= $this->class($parse, $name, $comment); - $class->declare(new Method( + $class= $this->class($parse, $name, $comment, [], $token->line); + $class->body= ['__construct()' => new Method( ['public'], '__construct', new Signature($parameters, null, false, $token->line), @@ -926,9 +926,9 @@ public function __construct() { null, $comment, $token->line, - )); + )] + $class->body; } else { - $class= $this->class($parse, $name, $comment); + $class= $this->class($parse, $name, $comment, [], $token->line); } return $class; @@ -1771,8 +1771,7 @@ public function block($parse) { } } - public function class($parse, $name, $comment= null, $modifiers= []) { - $line= $parse->token->line; + public function class($parse, $name, $comment= null, $modifiers= [], $line= null) { $parent= null; if ('extends' === $parse->token->value) { $parse->forward(); From de80cff30e57c239add842c8f45b1bbd3aec8de8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 18:07:29 +0200 Subject: [PATCH 3/7] Ensure declaration order --- 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 5d53541..2bad69d 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -917,8 +917,7 @@ public function __construct() { $parameters= $this->parameters($parse); $parse->expecting(')', 'class'); - $class= $this->class($parse, $name, $comment, [], $token->line); - $class->body= ['__construct()' => new Method( + $constructor= new Method( ['public'], '__construct', new Signature($parameters, null, false, $token->line), @@ -926,7 +925,9 @@ public function __construct() { null, $comment, $token->line, - )] + $class->body; + ); + $class= $this->class($parse, $name, $comment, [], $token->line); + $class->body= ['__construct()' => $constructor] + $class->body; } else { $class= $this->class($parse, $name, $comment, [], $token->line); } From 5c243dbc7d447f4c66a6df10efacd55edc7c1f24 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 18:32:57 +0200 Subject: [PATCH 4/7] Check for call to parent constructor via `extends B(...)` --- src/main/php/lang/ast/syntax/PHP.class.php | 21 ++++++++++++++++++++- 1 file changed, 20 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 2bad69d..07f63fa 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -917,17 +917,36 @@ public function __construct() { $parameters= $this->parameters($parse); $parse->expecting(')', 'class'); + // Check for call to parent constructor via `extends B(...)`: + $parent= null; + $statements= []; + if ('extends' === $parse->token->value) { + $parse->forward(); + $parent= $this->type($parse, false); + + if ('(' === $parse->token->value) { + $parse->expecting('(', 'base arguments'); + [$arguments, $callable]= $this->arguments($parse); + $parse->expecting(')', 'base arguments'); + $statements= [new ScopeExpression(new Literal('parent'), new InvokeExpression( + new Literal('__construct'), + $arguments + ))]; + } + } + $constructor= new Method( ['public'], '__construct', new Signature($parameters, null, false, $token->line), - new Block([], $token->line), + new Block($statements, $token->line), null, $comment, $token->line, ); $class= $this->class($parse, $name, $comment, [], $token->line); $class->body= ['__construct()' => $constructor] + $class->body; + $class->parent= $parent; } else { $class= $this->class($parse, $name, $comment, [], $token->line); } From 9fd70edde55842242f1f4aa8afc11bcd01ab0bba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 19:12:37 +0200 Subject: [PATCH 5/7] Add test for primary constructor parsing --- .../ast/unittest/parse/TypesTest.class.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) 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 cafb2cb..5096ecc 100755 --- a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php @@ -2,16 +2,21 @@ use lang\ast\Errors; use lang\ast\nodes\{ + Block, ClassDeclaration, InterfaceDeclaration, EnumDeclaration, EnumCase, + Method, NamespaceDeclaration, + Parameter, + Property, + Signature, TraitDeclaration, UseExpression, Literal }; -use lang\ast\types\IsValue; +use lang\ast\types\{IsValue, IsLiteral}; use test\{Assert, Expect, Test}; class TypesTest extends ParseTest { @@ -173,6 +178,32 @@ public function class_with_trait_and_aliases() { }'); } + #[Test] + public function primary_constructor() { + $lo= new Property(['public'], 'lo', new IsLiteral('int'), null, null, null, self::LINE); + $hi= new Property(['public'], 'hi', new IsLiteral('int'), null, null, null, self::LINE); + $class= new ClassDeclaration([], new IsValue('\\Range'), null, [], [], null, null, self::LINE); + $class->declare(new Method( + ['public'], + '__construct', + new Signature( + [ + new Parameter('lo', new IsLiteral('int'), null, false, false, $lo, null, null, self::LINE), + new Parameter('hi', new IsLiteral('int'), null, false, false, $hi, null, null, self::LINE), + ], + null, + false, + self::LINE + ), + new Block([], self::LINE), + null, + null, + self::LINE + )); + + $this->assertParsed([$class], 'class Range(public int $lo, public int $hi) { }'); + } + #[Test] public function class_in_namespace() { $this->assertParsed( From eddd6e0df9fd404cdfe6a19df3e5a8a7b7505e84 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 19:18:11 +0200 Subject: [PATCH 6/7] Simplify parsing anonymous classes --- src/main/php/lang/ast/syntax/PHP.class.php | 26 +++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 07f63fa..137c257 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -346,26 +346,22 @@ public function __construct() { [$arguments, $callable]= $this->arguments($parse); $parse->expecting(')', 'new arguments'); + // Anonymous classes + if (null === $type) { + $class= $this->class($parse, null, null, [], $parse->token->line); + $class->annotations= $annotations; + $new= new NewClassExpression($class, null, $token->line); + } else { + $new= new NewExpression($type, null, $token->line); + } + // Resolve ambiguity by looking ahead: `new T(...)` which is a first-class // callable reference vs. `new T(...$it)` - a call with an unpacked argument if ($callable) { - if (null === $type) { - $class= $this->class($parse, null, null, [], $parse->token->line); - $class->annotations= $annotations; - $new= new NewClassExpression($class, null, $token->line); - } else { - $new= new NewExpression($type, null, $token->line); - } - return new CallableNewExpression($new, $arguments, $token->line); - } - - if (null === $type) { - $class= $this->class($parse, null, null, [], $parse->token->line); - $class->annotations= $annotations; - return new NewClassExpression($class, $arguments, $token->line); } else { - return new NewExpression($type, $arguments, $token->line); + $new->arguments= $arguments; + return $new; } }); From 1b2898937c255fc424ca683d0ebb2066e6f02b1f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 5 Jul 2026 19:44:11 +0200 Subject: [PATCH 7/7] Use string for type inside ScopeExpression --- 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 137c257..0ee6813 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -924,7 +924,7 @@ public function __construct() { $parse->expecting('(', 'base arguments'); [$arguments, $callable]= $this->arguments($parse); $parse->expecting(')', 'base arguments'); - $statements= [new ScopeExpression(new Literal('parent'), new InvokeExpression( + $statements= [new ScopeExpression('parent', new InvokeExpression( new Literal('__construct'), $arguments ))];