Skip to content

Implement first-class callable syntax #114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ 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

## 6.6.0 / 2021-07-10

* Emit null-coalesce operator as `$a ?? $a= expression` instead of as
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "^7.4",
"php" : ">=7.0.0"
},
"require-dev" : {
Expand Down
54 changes: 54 additions & 0 deletions src/main/php/lang/ast/emit/CallablesAsClosures.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php namespace lang\ast\emit;

use lang\ast\Node;
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal};

/**
* Rewrites callable expressions to `Callable::fromClosure()`
*
* @see https://wiki.php.net/rfc/first_class_callable_syntax
*/
trait CallablesAsClosures {

protected function emitCallable($result, $callable) {
$result->out->write('\Closure::fromCallable(');
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", "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(')');
}
}
8 changes: 6 additions & 2 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -946,6 +945,11 @@ protected function emitNewClass($result, $new) {
array_shift($result->type);
}

protected function emitCallable($result, $callable) {
$this->emitOne($result, $callable->expression);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the RFC merged to PHP 8.1, we should move this to a trait for PHP 7.4 and 8.0 and emit native code on PHP 8.1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the only thing we need to do is to remove the use CallablesAsClosures from the PHP81 emitter.

$result->out->write('(...)');
}

protected function emitInvoke($result, $invoke) {
$this->emitOne($result, $invoke->expression);
$result->out->write('(');
Expand Down
49 changes: 49 additions & 0 deletions src/main/php/lang/ast/emit/PHP70.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php namespace lang\ast\emit;

use lang\ast\Node;
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Literal};
use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsValue, IsLiteral};

/**
Expand All @@ -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"); })())');
}
}
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP71.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, RewriteEnums;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP72.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, RewriteEnums;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP74.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_74
*/
class PHP74 extends PHP {
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums;
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP80.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @see https://wiki.php.net/rfc#php_80
*/
class PHP80 extends PHP {
use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums;
use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP81.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
152 changes: 152 additions & 0 deletions src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php namespace lang\ast\unittest\emit;

use lang\Error;
use unittest\{Assert, Expect, 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() {
$this->verify('class <T> {
public function run() { return strlen(...); }
}');
}

#[Test]
public function instance_method() {
$this->verify('class <T> {
public function length($arg) { return strlen($arg); }
public function run() { return $this->length(...); }
}');
}

#[Test]
public function class_method() {
$this->verify('class <T> {
public static function length($arg) { return strlen($arg); }
public function run() { return self::length(...); }
}');
}

#[Test]
public function private_method() {
$this->verify('class <T> {
private function length($arg) { return strlen($arg); }
public function run() { return $this->length(...); }
}');
}

#[Test]
public function string_reference() {
$this->verify('class <T> {
public function run() {
$func= "strlen";
return $func(...);
}
}');
}

#[Test]
public function fn_reference() {
$this->verify('class <T> {
public function run() {
$func= fn($arg) => strlen($arg);
return $func(...);
}
}');
}

#[Test]
public function instance_property_reference() {
$this->verify('class <T> {
private $func= "strlen";
public function run() {
return ($this->func)(...);
}
}');
}

#[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])]
public function variable_instance_method($expr) {
$this->verify('class <T> {
private function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
return '.$expr.';
}
}');
}

#[Test, Values(['self::$func(...)', 'self::{$func}(...)'])]
public function variable_class_method($expr) {
$this->verify('class <T> {
private static function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
return '.$expr.';
}
}');
}

#[Test]
public function variable_class_method_with_variable_class() {
$this->verify('class <T> {
private static function length($arg) { return strlen($arg); }
public function run() {
$func= "length";
$class= __CLASS__;
return $class::$func(...);
}
}');
}

#[Test]
public function string_function_reference() {
$this->verify('class <T> {
public function run() { return "strlen"(...); }
}');
}

#[Test]
public function array_instance_method_reference() {
$this->verify('class <T> {
public function length($arg) { return strlen($arg); }
public function run() { return [$this, "length"](...); }
}');
}

#[Test]
public function array_class_method_reference() {
$this->verify('class <T> {
public static function length($arg) { return strlen($arg); }
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 <T> {
public function run() {
$null= null;
$nonexistant= "nonexistant";
return '.$expr.'(...);
}
}');
}
}