Skip to content
Open
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
9 changes: 9 additions & 0 deletions phpunit/code/closure-param-type-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
class MyClass
{
public function method(): int
{
$fn = fn(int $x) => $x + 1;
return $fn(42);
}
}
186 changes: 186 additions & 0 deletions phpunit/code/closure-param-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php
/**
* Test fixture for closure parameter type narrowing.
*
* Tests:
* 1. Type declaration inference (int, float, bool, string, array)
* 2. Call-site literal inference (int, float, bool, array)
* 3. Multi-call fallback to php::Var
* 4. UnaryMinus/UnaryPlus expressions
* 5. Boolean expressions (===, ||, instanceof)
* 6. String concatenation
* 7. goto invalidates candidates
* 8. Nested functions (still narrowed)
* 9. Cast expressions
*/

// --- Type declaration inference ---
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);
}

// --- Call-site literal inference ---
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]);
}

// --- Multi-call fallback ---
function closureMultiCallNoInfer(): void
{
$fn = fn($x) => $x + 1;
var_dump($fn(42));
var_dump($fn(3.14));
}

// --- UnaryMinus/UnaryPlus ---
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);
}

// --- Boolean expressions ---
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);
}

// --- String concatenation ---
function closureCallSiteConcat(): string
{
$fn = fn($x) => $x;
return $fn("hello" . "world");
}

function closureCallSiteConcatWithInt(): string
{
$fn = fn($x) => $x;
return $fn("hello" . 42);
}

// --- Cast expressions ---
function closureCallSiteCastInt(): int
{
$fn = fn($x) => $x + 1;
return $fn((int)"42");
}

function closureCallSiteCastString(): string
{
$fn = fn($x) => $x;
return $fn((string)42);
}

// --- goto invalidates candidates ---
function closureWithGoto(): void
{
$fn = fn($x) => $x + 1;
var_dump($fn(1));
goto end;
end:
}

// --- Nested functions (still narrowed) ---
function closureWithNestedFn(): int
{
$fn = fn($x) => $x + 1;
return $fn(42);
}

// --- Entry point ---
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();
closureCallSiteConcatWithInt();
closureCallSiteCastInt();
closureCallSiteCastString();
closureWithGoto();
closureWithNestedFn();
}
162 changes: 162 additions & 0 deletions phpunit/src/ClosureParamTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/

use TypePhp\CompilerTest;

/**
* @internal
* @coversNothing
*/
final class ClosureParamTypeTest extends BaseTest
{
private function compileFixture(string $fixture): string
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $fixture;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
return file_get_contents($compiler->convertFile($source));
}

public function testTypeHintIntInfersNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
}

public function testTypeHintFloatInfersNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Float x)', $code);
}

public function testTypeHintBoolInfersNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Bool x)', $code);
}

public function testTypeHintStringInfersNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Str x)', $code);
}

public function testTypeHintArrayInfersNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Array x)', $code);
}

public function testCallSiteIntLiteralInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
}

public function testCallSiteFloatLiteralInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Float x)', $code);
}

public function testCallSiteBoolLiteralInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Bool x)', $code);
}

public function testCallSiteArrayLiteralInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Array x)', $code);
}

public function testMultiCallFallsBackToVar(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Var x)', $code);
}

public function testNegIntInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
}

public function testNegFloatInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Float x)', $code);
}

public function testUnaryPlusInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
}

public function testBoolExprInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Bool x)', $code);
}

public function testLogicalOrInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Bool x)', $code);
}

public function testInstanceofInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Bool x)', $code);
}

public function testConcatStringInference(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Str x)', $code);
}

public function testConcatWithNonStringOperandInfersString(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Str x)', $code);
}

public function testCastExpressionsInferNativeType(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
self::assertStringContainsString('(php::Str x)', $code);
}

public function testGotoInvalidatesAllCandidates(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('newClosureWithParameters', $code);
}

public function testNestedFnStillWorks(): void
{
$code = $this->compileFixture('closure-param-type.php');
self::assertStringContainsString('(php::Int x)', $code);
}

public function testClassMethodClosureStaysZend(): void
{
$code = $this->compileFixture('closure-param-type-class.php');
self::assertStringNotContainsString('(php::Int x)', $code);
self::assertStringContainsString('newClosureWithParameters', $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
Loading
Loading