Skip to content
Closed
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
113 changes: 113 additions & 0 deletions phpunit/code/closure-param-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
function closureTypeHintInt(int $x): int
{
$fn = fn(int $a) => $a + 1;
return $fn($x);
}
function closureTypeHintFloat(float $x): float
{
$fn = fn(float $a) => $a * 2.0;
return $fn($x);
}
function closureTypeHintBool(bool $x): bool
{
$fn = fn(bool $a) => !$a;
return $fn($x);
}
function closureTypeHintString(string $x): int
{
$fn = fn(string $a) => strlen($a);
return $fn($x);
}
function closureTypeHintArray(array $x): int
{
$fn = fn(array $a) => count($a);
return $fn($x);
}
function closureCallSiteInt(): int
{
$fn = fn($x) => $x + 1;
return $fn(42);
}
function closureCallSiteFloat(): float
{
$fn = fn($x) => $x * 2.0;
return $fn(3.14);
}
function closureCallSiteBool(): bool
{
$fn = fn($x) => !$x;
return $fn(true);
}
function closureCallSiteArray(): int
{
$fn = fn($arr) => count($arr);
return $fn([1, 2, 3, 4, 5]);
}
function closureMultiCallNoInfer(): void
{
$fn = fn($x) => $x + 1;
var_dump($fn(42));
var_dump($fn(3.14));
}
function closureCallSiteNegInt(): int
{
$fn = fn($x) => $x + 1;
return $fn(-42);
}
function closureCallSiteNegFloat(): float
{
$fn = fn($x) => $x * 2.0;
return $fn(-3.14);
}
function closureCallSiteUnaryPlus(): int
{
$fn = fn($x) => $x + 1;
return $fn(+42);
}
function closureCallSiteBoolExpr(): bool
{
$fn = fn($x) => !$x;
return $fn(1 === 2);
}
function closureCallSiteLogicalOr(): bool
{
$fn = fn($x) => $x;
return $fn(true || false);
}
function closureCallSiteInstanceof(): bool
{
$fn = fn($x) => $x;
return $fn(new \stdClass() instanceof \stdClass);
}
function closureCallSiteConcat(): string
{
$fn = fn($x) => $x;
return $fn("hello" . "world");
}
function closureCallSiteConcatMixed(): string
{
$fn = fn($x) => $x;
return $fn("hello" . 123);
}
function main(): void
{
closureTypeHintInt(10);
closureTypeHintFloat(1.5);
closureTypeHintBool(false);
closureTypeHintString("test");
closureTypeHintArray([1, 2]);
closureCallSiteInt();
closureCallSiteFloat();
closureCallSiteBool();
closureCallSiteArray();
closureMultiCallNoInfer();
closureCallSiteNegInt();
closureCallSiteNegFloat();
closureCallSiteUnaryPlus();
closureCallSiteBoolExpr();
closureCallSiteLogicalOr();
closureCallSiteInstanceof();
closureCallSiteConcat();
closureCallSiteConcatMixed();
}
110 changes: 110 additions & 0 deletions phpunit/src/ClosureParamTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

use TypePhp\CompilerTest;

final class ClosureParamTypeTest extends BaseTest
{
public function testTypeHintParametersUseNativeCppTypes(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Int a)', $code);
self::assertStringContainsString('(php::Float a)', $code);
self::assertStringContainsString('(php::Bool a)', $code);
self::assertStringContainsString('(php::Str a)', $code);
self::assertStringContainsString('(php::Array a)', $code);
}

public function testCallSiteInferredParametersUseNativeCppTypes(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Int x)', $code);
self::assertStringContainsString('(php::Float x)', $code);
self::assertStringContainsString('(php::Bool x)', $code);
self::assertStringContainsString('(php::Array arr)', $code);
}

public function testMultiCallClosureRemainsPhpVar(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Var x)', $code);
}

public function testUnaryMinusInfersNativeType(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Int x)', $code);
self::assertStringContainsString('(php::Float x)', $code);
}

public function testBooleanExpressionsInferBoolType(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Bool x)', $code);
}

public function testConcatStringInference(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));

self::assertIsString($code);

self::assertStringContainsString('(php::Str x)', $code);
}
}
2 changes: 1 addition & 1 deletion phpunit/src/LocalClosureCodegenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function testOnlyProvenLocalClosuresUseConcreteCppLambdas(): void

self::assertIsString($code);
self::assertStringContainsString(
'auto direct = [base = base](php::Var value) mutable -> php::Var {',
'auto direct = [base = base](php::Int value) mutable -> php::Var {',
$code,
);
self::assertStringContainsString('direct(2L)', $code);
Expand Down
100 changes: 98 additions & 2 deletions src/Analysis/LocalClosureAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PhpParser\Node\Expr;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use TypePhp\Type;

/**
* Proves the deliberately small set of local Closures which can stay entirely
Expand All @@ -20,7 +21,7 @@
*/
final class LocalClosureAnalyzer
{
/** @var array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}> */
/** @var array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int, callSites: list<Expr\FuncCall>}> */
private array $candidates = [];

/** @var array<string, true> */
Expand All @@ -31,7 +32,7 @@ final class LocalClosureAnalyzer

/**
* @param list<Stmt> $statements
* @return array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}>
* @return array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int, callSites: list<Expr\FuncCall>}>
*/
public function analyze(array $statements): array
{
Expand Down Expand Up @@ -63,6 +64,7 @@ public function analyze(array $statements): array
'assignment' => $statement->expr,
'closure' => $statement->expr->expr,
'calls' => 0,
'callSites' => [],
];
}

Expand Down Expand Up @@ -205,6 +207,7 @@ private function classifyVariableUse(
}

$this->candidates[$name]['calls']++;
$this->candidates[$name]['callSites'][] = $parent;
}

private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount): bool
Expand All @@ -219,4 +222,97 @@ private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount)
}
return true;
}

public function inferParamTypes(array $candidate): array
{
$closure = $candidate['closure'];
$paramCount = count($closure->params);
$callSites = $candidate['callSites'];

if (count($callSites) !== 1) {
return array_fill(0, $paramCount, Type::VAR);
}

$call = $callSites[0];
$inferredTypes = [];

foreach ($call->args as $i => $arg) {
$type = $this->detectArgType($arg->value);
$inferredTypes[$i] = $type;
}

return $inferredTypes;
}

private function detectArgType(Expr $expr): string
{
if ($expr instanceof Node\Scalar\Int_) {
return Type::INT;
}

if ($expr instanceof Node\Scalar\Float_) {
return Type::FLOAT;
}

if ($expr instanceof Node\Scalar\String_) {
return Type::STR;
}

if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) {
return $this->detectArgType($expr->expr);
}

if ($expr instanceof Expr\BooleanNot
|| $expr instanceof Expr\BinaryOp\BooleanAnd
|| $expr instanceof Expr\BinaryOp\BooleanOr
|| $expr instanceof Expr\BinaryOp\LogicalAnd
|| $expr instanceof Expr\BinaryOp\LogicalOr
|| $expr instanceof Expr\BinaryOp\Identical
|| $expr instanceof Expr\BinaryOp\NotIdentical
|| $expr instanceof Expr\BinaryOp\Equal
|| $expr instanceof Expr\BinaryOp\NotEqual
|| $expr instanceof Expr\BinaryOp\Smaller
|| $expr instanceof Expr\BinaryOp\SmallerOrEqual
|| $expr instanceof Expr\BinaryOp\Greater
|| $expr instanceof Expr\BinaryOp\GreaterOrEqual
|| $expr instanceof Expr\BinaryOp\Spaceship
|| $expr instanceof Expr\Instanceof_
) {
return Type::BOOL;
}

if ($expr instanceof Expr\BinaryOp\Concat) {
$left = $this->detectArgType($expr->left);
$right = $this->detectArgType($expr->right);
if ($left === Type::STR && $right === Type::STR) {
return Type::STR;
}
return Type::VAR;
}

if ($expr instanceof Expr\ConstFetch && $expr->name instanceof Node\Name) {
$name = strtolower($expr->name->toString());
if ($name === 'true' || $name === 'false') {
return Type::BOOL;
}
return Type::VAR;
}

if ($expr instanceof Expr\Array_) {
return Type::ARRAY;
}

if ($expr instanceof Expr\Variable) {
return Type::VAR;
}

if ($expr instanceof Expr\FuncCall && $expr->name instanceof Node\Name) {
$name = strtolower($expr->name->toString());
if (in_array($name, ['count', 'strlen', 'sizeof'], true)) {
return Type::INT;
}
}

return Type::VAR;
}
}
Loading
Loading