xp-framework / compiler

Compiles future PHP to today's PHP.
19 stars 0 forks source link

Group use statements #108

Closed thekid closed 2 years ago

thekid commented 3 years ago

Currently, all use statements are emitted standalone:

// Source code
use lang\{Value, Runnable};

// Result
new UseStatement(null, ['lang\\Value' => null, 'lang\\Runnable' => null], $line);

// Emitted as
use lang\Value; use lang\Runnable;

The emitted code could be more efficient when using grouped use syntax available since PHP 7.0.

thekid commented 3 years ago

One option could be to parse grouped use statements into a new node GroupedUseStatement. This would however be a BC break.

thekid commented 3 years ago

Another idea would be to declare a base member with the base namespace (everything before the opening curly brace, lang in the above example) and set this member. Emitters knowing about this member could then fairly easily emit the grouped syntax.

Diff for ast

diff --git a/src/main/php/lang/ast/nodes/UseStatement.class.php b/src/main/php/lang/ast/nodes/UseStatement.class.php
index 3d91284..a31e557 100755
--- a/src/main/php/lang/ast/nodes/UseStatement.class.php
+++ b/src/main/php/lang/ast/nodes/UseStatement.class.php
@@ -4,11 +4,12 @@ use lang\ast\Node;

 class UseStatement extends Node {
   public $kind= 'import';
-  public $type, $names;
+  public $type, $names, $base;

-  public function __construct($type, $names, $line= -1) {
+  public function __construct($type, $names, $base= null, $line= -1) {
     $this->type= $type;
     $this->names= $names;
+    $this->base= $base;
     $this->line= $line;
   }
 }
\ 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 24a465d..789f479 100755
--- a/src/main/php/lang/ast/syntax/PHP.class.php
+++ b/src/main/php/lang/ast/syntax/PHP.class.php
@@ -545,6 +545,7 @@ class PHP extends Language {
       $parse->forward();

       if ('{' === $parse->token->value) {
+        $base= $import;
         $names= [];
         $parse->forward();
         while ('}' !== $parse->token->value) {
@@ -572,17 +573,19 @@ class PHP extends Language {
         }
         $parse->forward();
       } else if ('as' === $parse->token->value) {
+        $base= null;
         $parse->forward();
         $names= [$import => $parse->token->value];
         $parse->scope->import($import, $parse->token->value);
         $parse->forward();
       } else {
+        $base= null;
         $names= [$import => null];
         $parse->scope->import($import);
       }

       $parse->expecting(';', 'use');
-      return new UseStatement($type, $names, $token->line);
+      return new UseStatement($type, $names, $base, $token->line);
     });

     $this->stmt('if', function($parse, $token) {
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 37f3d2f..71ec6f1 100755
--- a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php
+++ b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php
@@ -24,7 +24,7 @@ class NamespacesTest extends ParseTest {
   #[Test]
   public function use_statement() {
     $this->assertParsed(
-      [new UseStatement(null, ['lang\ast\Parse' => null], self::LINE)],
+      [new UseStatement(null, ['lang\ast\Parse' => null], null, self::LINE)],
       'use lang\\ast\\Parse;'
     );
   }
@@ -32,7 +32,7 @@ class NamespacesTest extends ParseTest {
   #[Test]
   public function use_with_alias() {
     $this->assertParsed(
-      [new UseStatement(null, ['lang\ast\Parse' => 'P'], self::LINE)],
+      [new UseStatement(null, ['lang\ast\Parse' => 'P'], null, self::LINE)],
       'use lang\\ast\\Parse as P;'
     );
   }
@@ -40,7 +40,7 @@ class NamespacesTest extends ParseTest {
   #[Test]
   public function grouped_use_statement() {
     $this->assertParsed(
-      [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\Emitter' => null], self::LINE)],
+      [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\Emitter' => null], 'lang\\ast\\', self::LINE)],
       'use lang\\ast\\{Parse, Emitter};'
     );
   }
@@ -48,7 +48,7 @@ class NamespacesTest extends ParseTest {
   #[Test]
   public function grouped_use_with_relative() {
     $this->assertParsed(
-      [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\emit\\Result' => null], self::LINE)],
+      [new UseStatement(null, ['lang\\ast\\Parse' => null, 'lang\\ast\\emit\\Result' => null], 'lang\\ast\\', self::LINE)],
       'use lang\\ast\\{Parse, emit\\Result};'
     );
   }
@@ -56,7 +56,7 @@ class NamespacesTest extends ParseTest {
   #[Test]
   public function grouped_use_with_alias() {
     $this->assertParsed(
-      [new UseStatement(null, ['lang\\ast\\Parse' => 'P'], self::LINE)],
+      [new UseStatement(null, ['lang\\ast\\Parse' => 'P'], 'lang\\ast\\', self::LINE)],
       'use lang\\ast\\{Parse as P};'
     );
   }

Diff for compiler

diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php
index 1c4198e..901cfbb 100755
--- a/src/main/php/lang/ast/emit/PHP.class.php
+++ b/src/main/php/lang/ast/emit/PHP.class.php
@@ -162,8 +162,19 @@ abstract class PHP extends Emitter {
   }

   protected function emitImport($result, $import) {
-    foreach ($import->names as $name => $alias) {
-      $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';');
+    if ($import->base) {
+      $result->out->write('use '.$import->type.' '.$import->base.'{');
+      $l= strlen($import->base);
+      $i= 0;
+      foreach ($import->names as $name => $alias) {
+        $i++ && $result->out->write(',');
+        $result->out->write(substr($name, $l).($alias ? ' as '.$alias : ''));
+      }
+      $result->out->write('}');
+    } else {
+      foreach ($import->names as $name => $alias) {
+        $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';');
+      }
     }
   }
thekid commented 3 years ago

One option could be to parse grouped use statements into a new node GroupedUseStatement. This would however be a BC break.

This might be the cleaner alternative though, we might keep this here for the next major release in this case.

danon commented 3 years ago

@thekid Hello, are you looking for contributors?

thekid commented 3 years ago

@thekid Hello, are you looking for contributors?

Sure, help is always welcome 👍

Did you have a specific idea in mind what you would like to contribute or are you looking for things to be done? I have a small amount of issues tagged help wanted, and I'm keen on receiving feedback as to what you would want to see in this piece of software!

danon commented 3 years ago

@thekid Hello, are you looking for contributors?

Sure, help is always welcome 👍

Did you have a specific idea in mind what you would like to contribute or are you looking for things to be done? I have a small amount of issues tagged help wanted, and I'm keen on receiving feedback as to what you would want to see in this piece of software!

Yes and no.

First of all, I'm a library maintainer myself, and keeping backwards comaptible changes is hard. So when I saw your project, I thought to myself: "Hey, if this project kicks, I can develop my library using newest PHP 8.1 features, and still maintain full PHP 7 support.". So I saw areas where I could use your project.

Second thing is, my biggest advantage is being detail-oriented and finding corner-cases, I think that might be useful in this project.

And third, my biggest skill is writing good unit tests, and I think you might Look kindly on it.

thekid commented 3 years ago

I can develop my library using newest PHP 8.1 features, and still maintain full PHP 7 support

Yes, that would be possible! Maybe a word of warning: The generated code is quite ugly, is stripped of any comments and contains some XP Framework-related meta data:

image

One idea to make this more generically usable would be to make rewriting api doc comments and annotations to meta data optional, e.g. by regarding it an optimization (XP Framework will parse it from the class' sourcecode if not present). This way, your library wouldn't need an additional runtime dependency.

Maybe this would be a good starting point for a joint effort?

Second thing is, my biggest advantage is being detail-oriented and finding corner-cases, I think that might be useful in this project.

While working on the above project together, we would certainly find a lot of these 😉

And third, my biggest skill is writing good unit tests, and I think you might Look kindly on it.

Here's some statistics on coverage - the classes used by the command line tool could need some coverage love:

image

thekid commented 3 years ago

the classes used by the command line tool could need some coverage love

I've taken care of this on the meantime 😉 - but help on making the compiled code independent of XP core is greatly appreciated, see issue #116

danon commented 3 years ago

@thekid So what's the idea behind this project?

Did you write some ideas/concepts somewhere, so I can get familiar?

For example, if there are typed properties in PHP 8.0, but not in PHP 7.0, will you just ignore the types? Or will you write some polyfill that would validate the type?

thekid commented 3 years ago

So what's the idea behind this project?

I've written up a document on this @Danon - see here: https://github.com/xp-framework/compiler/wiki/Compiler-design.

Or will you write some polyfill that would validate the type?

I'm open to simulate typed properties with a combination of get and set (but cannot be done for static members, see https://bugs.php.net/bug.php?id=52225) - I think the performance overhead is okay-ish.

thekid commented 2 years ago

The emitted code could be more efficient when using grouped use syntax available since PHP 7.0.

However, the emitting code is much more complex if we try to do this without BC breaks.