From 1a5e7e1bdd7ca9bf64bf396096c22ed08c1ae5b1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 27 Mar 2024 13:32:06 +0100 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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" : {