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
59 changes: 59 additions & 0 deletions phpunit/code/match-expression-codegen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/

function testUniformIntMatch(int $x): int
{
return match ($x) {
1 => 10,
2 => 20,
default => 30,
};
}

function testUniformStringMatch(int $x): string
{
return match ($x) {
1 => 'first',
2 => 'second',
default => 'other',
};
}

function testMatchWithThrow(int $x): int
{
return match ($x) {
1 => 10,
2 => 20,
default => throw new \RuntimeException('Invalid choice'),
};
}

function testMatchNoDefault(int $x): int
{
return match ($x) {
1 => 100,
2 => 200,
};
}

function testMixedTypeMatch(int $x): mixed
{
return match ($x) {
1 => 10,
default => 'other',
};
}

function testMatchLocalVariableAssignment(int $x): void
{
$result = match ($x) {
1 => 10,
default => 20,
};
var_dump($result);
}
112 changes: 112 additions & 0 deletions phpunit/src/MatchExpressionCodegenTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?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 MatchExpressionCodegenTest extends \BaseTest
{
private string $generatedCode;

protected function setUp(): void
{
parent::setUp();

global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/match-expression-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$code = file_get_contents($compiler->convertFile($source));
self::assertIsString($code);
$this->generatedCode = $code;
}

public function testUniformIntMatchInfersIntAndAvoidsRuntimeReturnTypeCheck(): void
{
// Must emit typed lambda returning php::Int
self::assertStringContainsString('php_testuniformintmatch', $this->generatedCode);
self::assertStringContainsString('[&]() -> php::Int {', $this->generatedCode);

// Function body must not use php::Var temporary or throwReturnTypeError for uniform int match
$fnCode = $this->extractFunction('php_testuniformintmatch');
self::assertStringNotContainsString('php::Var', $fnCode);
self::assertStringNotContainsString('php::throwReturnTypeError', $fnCode);
self::assertMatchesRegularExpression('/return 10L{1,2};/', $fnCode);
self::assertMatchesRegularExpression('/return 20L{1,2};/', $fnCode);
self::assertMatchesRegularExpression('/return 30L{1,2};/', $fnCode);
}

public function testUniformStringMatchInfersString(): void
{
self::assertStringContainsString('php_testuniformstringmatch', $this->generatedCode);
$fnCode = $this->extractFunction('php_testuniformstringmatch');
self::assertStringContainsString('[&]() -> php::Str {', $fnCode);
self::assertStringNotContainsString('php::Var', $fnCode);
self::assertStringNotContainsString('php::throwReturnTypeError', $fnCode);
}

public function testMatchWithThrowArmPreservesInferredReturnType(): void
{
$fnCode = $this->extractFunction('php_testmatchwiththrow');
self::assertStringContainsString('[&]() -> php::Int {', $fnCode);
self::assertStringContainsString('php::throwException(', $fnCode);
self::assertStringContainsString('return {};', $fnCode);
}

public function testMatchNoDefaultEmitsTypedUnhandledMatchCase(): void
{
$fnCode = $this->extractFunction('php_testmatchnodefault');
self::assertStringContainsString('[&]() -> php::Int {', $fnCode);
self::assertStringContainsString('php::throwException("UnhandledMatchError", "Unhandled match case");', $fnCode);
self::assertStringContainsString('return {};', $fnCode);
}

public function testMixedTypeMatchFallsBackToVariant(): void
{
$fnCode = $this->extractFunction('php_testmixedtypematch');
self::assertStringContainsString('[&]() -> php::Var {', $fnCode);
self::assertStringContainsString('php::Var tmp_var_', $fnCode);
}

public function testMatchExpressionLocalVariableAssignment(): void
{
$fnCode = $this->extractFunction('php_testmatchlocalvariableassignment');
// $result must be inferred as php::Int rather than php::Var
self::assertStringContainsString('php::Int result = 0;', $fnCode);
self::assertStringNotContainsString('php::Var result;', $fnCode);
}

private function extractFunction(string $name): string
{
$pos = strpos($this->generatedCode, $name);
self::assertNotFalse($pos, "Function {$name} not found in generated code");
$braceStart = strpos($this->generatedCode, '{', $pos);
self::assertNotFalse($braceStart);

$depth = 0;
$len = strlen($this->generatedCode);
for ($i = $braceStart; $i < $len; $i++) {
$char = $this->generatedCode[$i];
if ($char === '{') {
$depth++;
} elseif ($char === '}') {
$depth--;
if ($depth === 0) {
return substr($this->generatedCode, $pos, $i - $pos + 1);
}
}
}

return substr($this->generatedCode, $pos, 1000);
}
}
19 changes: 19 additions & 0 deletions src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3551,6 +3551,25 @@ protected function detectTypeOfExpr($expr): string
: $this->detectTypeOfExpr($expr->if);
$elseType = $this->detectTypeOfExpr($expr->else);
return $ifType === $elseType ? $ifType : Type::VAR;
case 'Expr_Match':
if (empty($expr->arms)) {
return Type::VAR;
}
$armTypes = [];
foreach ($expr->arms as $arm) {
if ($arm->body instanceof Expr\Throw_) {
continue;
}
$armTypes[] = Type::getReferencedType($this->detectTypeOfExpr($arm->body));
}
if (empty($armTypes)) {
return Type::VAR;
}
$uniqueTypes = array_unique($armTypes);
if (count($uniqueTypes) === 1) {
return reset($uniqueTypes);
}
return Type::VAR;
case 'Expr_BinaryOp_Plus':
case 'Expr_BinaryOp_Minus':
case 'Expr_BinaryOp_Mul':
Expand Down
46 changes: 35 additions & 11 deletions src/Parser/SelectionExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,13 @@ protected function parseMatch(Expr\Match_ $expr): string
$nativeSelection = $this->isNativeObjectClass($nativeClass);
$conditionNativeClass = $this->detectClassOfExpr($expr->cond);
$nativeCondition = $this->isNativeObjectClass($conditionNativeClass);
$returnType = $nativeSelection
? $this->getNativeObjectPointerType($nativeClass)
: Type::VAR;
if ($nativeSelection) {
$returnType = $this->getNativeObjectPointerType($nativeClass);
} else {
$inferredType = $this->detectTypeOfExpr($expr);
$returnType = $this->getNormalAssignType($inferredType);
}
$expr->setAttribute(self::ATTR_LOWERED_TYPE, $returnType);
$this->assertExprCanBeUsedAsValue($expr->cond, 'match condition');
$var = $this->parseIdentifier($expr->cond);
if ($this->isVarExpr($expr->cond)) {
Expand Down Expand Up @@ -220,24 +224,29 @@ protected function parseMatch(Expr\Match_ $expr): string
}
$code .= $this->getIndent() . 'if (' . $matched . ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->formatMatchReturn($arm->body, $nativeClass);
$code .= $this->formatMatchReturn($arm->body, $nativeClass, $returnType);
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}

if ($default) {
$code .= $this->getIndent() . '{' . PHP_EOL;
$this->indentLevel++;
$code .= $this->formatMatchReturn($default, $nativeClass);
$code .= $this->formatMatchReturn($default, $nativeClass, $returnType);
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
} else {
$code .= $this->getIndent() . '{' . PHP_EOL;
$this->indentLevel++;
$code .= $nativeSelection
? $this->getIndent() . 'php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL
. $this->getIndent() . 'return nullptr;' . PHP_EOL
: $this->getIndent() . 'return php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL;
if ($nativeSelection) {
$code .= $this->getIndent() . 'php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL
. $this->getIndent() . 'return nullptr;' . PHP_EOL;
} elseif ($returnType === Type::VAR) {
$code .= $this->getIndent() . 'return php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL;
} else {
$code .= $this->getIndent() . 'php::throwException("UnhandledMatchError", "Unhandled match case");' . PHP_EOL
. $this->getIndent() . 'return {};' . PHP_EOL;
}
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
Expand All @@ -247,9 +256,24 @@ protected function parseMatch(Expr\Match_ $expr): string
return $code;
}

protected function formatMatchReturn(NodeAbstract $body, string $nativeClass = ''): string
protected function formatMatchReturn(NodeAbstract $body, string $nativeClass = '', string $returnType = Type::VAR): string
{
$this->assertExprCanBeUsedAsValue($body, 'match arm');
if ($body instanceof Expr\Throw_) {
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($body);
$code = $this->formatCapturedStmtLines($beforeStmts);
$code .= $this->formatCapturedStmtLines($afterStmts);
if ($nativeClass !== '') {
$code .= $this->getIndent() . $value . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return nullptr;' . PHP_EOL;
} elseif ($returnType === Type::VAR) {
$code .= $this->getIndent() . 'return ' . $value . ';' . PHP_EOL;
} else {
$code .= $this->getIndent() . $value . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return {};' . PHP_EOL;
}
return $code;
}
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($body);
if ($nativeClass !== '' && $this->isNull($body)) {
$value = 'nullptr';
Expand All @@ -261,7 +285,7 @@ protected function formatMatchReturn(NodeAbstract $body, string $nativeClass = '
$this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass));
$this->addNativeObject($tmpVar, $nativeClass);
} else {
$tmpVar = $this->addTmpVar(Type::VAR);
$tmpVar = $this->addTmpVar($returnType);
}
$code .= $this->getIndent() . "{$tmpVar} = {$value};" . PHP_EOL;
$code .= $this->formatCapturedStmtLines($afterStmts);
Expand Down
Loading