Skip to content
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
11 changes: 11 additions & 0 deletions phpunit/code/unary-plus-codegen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

function positiveInteger(int $number): int
{
return +$number;
}

function positiveFloat(float $number): float
{
return +$number;
}
24 changes: 24 additions & 0 deletions phpunit/src/UnaryPlusCodegenTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use TypePhp\CompilerTest;

final class UnaryPlusCodegenTest extends \BaseTest
{
public function testNativeNumericUnaryPlusDoesNotBoxItsOperand(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/unary-plus-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);

self::assertIsString($code);
self::assertStringNotContainsString('php::Variant(number)', $code);
self::assertStringContainsString('php::toInt(number)', $code);
self::assertStringContainsString('php::toFloat(number)', $code);
}
}
12 changes: 11 additions & 1 deletion src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3369,8 +3369,18 @@ protected function detectTypeOfExpr($expr): string

$exprType = $expr->getType();
switch ($exprType) {
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
$constant = $this->constantUnaryPlusValue($expr);
if ($constant !== null) {
return is_float($constant) ? Type::FLOAT : Type::INT;
}
$innerType = $this->unaryPlusOperandType($expr->expr);
return match ($innerType) {
Type::BOOL => Type::INT,
Type::INT, Type::FLOAT, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL => $innerType,
default => Type::VAR,
};
case 'Expr_UnaryMinus':
$innerType = $this->detectTypeOfExpr($expr->expr);
if (
$this->varIntTypes
Expand Down
68 changes: 67 additions & 1 deletion src/Parser/UnaryExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,56 @@

trait UnaryExpressionTrait
{
protected function unaryPlusOperandType(Expr $operand): string
{
if ($operand instanceof Expr\ErrorSuppress) {
return $this->unaryPlusOperandType($operand->expr);
}
if ($operand instanceof Expr\Assign) {
if ($operand->var instanceof Expr\Variable
&& is_string($operand->var->name)
&& !$this->hasVar($this->parseVariable($operand->var))) {
// A fresh local has no target type until assignment registers it.
return $this->unaryPlusOperandType($operand->expr);
}
// Assignment evaluates to the value after conversion to its target
// type, which can differ from the RHS (e.g. int to float).
return $this->unaryPlusOperandType($operand->var);
}
if ($operand instanceof Expr\Ternary) {
$ifType = $this->unaryPlusOperandType($operand->if ?? $operand->cond);
$elseType = $this->unaryPlusOperandType($operand->else);
return $ifType === $elseType ? $ifType : Type::VAR;
}
$type = $this->detectTypeOfExpr($operand);
if ($operand instanceof Expr\PropertyFetch && $this->isIdExpr($operand->name)) {
$class = $this->resolveObjectClassDef($operand->var);
$name = $operand->name->toString();
if ($class !== null && $class->hasProperty($name) && $class->getProperty($name)->nullable) {
return Type::VAR;
}
}
if ($operand instanceof Expr\StaticPropertyFetch && $this->getNativePropertyDef($operand)?->nullable) {
return Type::VAR;
}
return $type;
}

protected function constantUnaryPlusValue(Expr\UnaryPlus $expr): int|float|null
{
if ($expr->expr instanceof \PhpParser\Node\Scalar\String_ && is_numeric($expr->expr->value)) {
return +$expr->expr->value;
}
if ($expr->expr instanceof Expr\ConstFetch) {
return match (strtolower($expr->expr->name->toString())) {
'true' => 1,
'false', 'null' => 0,
default => null,
};
}
return null;
}

protected function parseCastVoid(Expr\Cast\Void_ $node): string
{
if (!$node->getAttribute(VoidCastValidationVisitor::ALLOWED_ATTRIBUTE, false)) {
Expand Down Expand Up @@ -148,6 +198,22 @@ protected function parseUnaryPlus(Expr\UnaryPlus $expr): string
}
$this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '+', true);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
return $this->parseExprAsValue($expr->expr);
$type = $this->unaryPlusOperandType($expr->expr);
$constant = $this->constantUnaryPlusValue($expr);
if ($constant !== null) {
return is_float($constant)
? $this->genFloatLiteral($constant)
: $this->genIntegerLiteral($constant);
}
$code = $this->parseExprAsValue($expr->expr);
if ($type === Type::BOOL) {
return $this->convertIntExpr($code);
}
if (in_array($type, [Type::INT, Type::FLOAT, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
return $code;
}
// Zend lowers unary plus to multiplication by one, preserving numeric
// conversion, warnings, TypeError, and the sign of floating-point zero.
return '(php::Variant(' . $code . ') * 1)';
}
}
1 change: 0 additions & 1 deletion src/Preprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2594,7 +2594,6 @@ protected function detectDefaultValueType(NodeAbstract $node, ?string $scopeClas
case 'Expr_Array':
return 'array';
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch':
return match (strtolower($node->name->toString())) {
Expand Down
40 changes: 40 additions & 0 deletions tests/compiler/operator/unary-plus-assignment.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Unary plus uses the converted assignment result for typed properties
--FILE--
<?php
class UnaryAssignmentBox {
public float $number = 0.0;
public ?float $optional = null;
public static float $shared = 0.0;
public static ?float $nullableShared = null;
}
function assignedOnce(int &$calls): int { ++$calls; return 2; }
function existingTarget(float $int): int|float { return +($int = 2); }
function main() {
$box = new UnaryAssignmentBox();
var_dump(+($box->number = 2), $box->number);
var_dump(+(UnaryAssignmentBox::$shared = 3), UnaryAssignmentBox::$shared);
var_dump(+($box->optional = 4), +($box->optional = null));
var_dump(+(UnaryAssignmentBox::$nullableShared = 5), +(UnaryAssignmentBox::$nullableShared = null));
var_dump(+@($box->number = 6));
$calls = 0;
var_dump(+($box->number = assignedOnce($calls)), $calls);
var_dump(+($local = 7), $local);
var_dump(existingTarget(0.0));
}
?>
--EXPECT--
float(2)
float(2)
float(3)
float(3)
float(4)
int(0)
float(5)
int(0)
float(6)
float(2)
int(1)
int(7)
int(7)
float(2)
58 changes: 58 additions & 0 deletions tests/compiler/operator/unary-plus-conversion.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
--TEST--
Unary plus preserves PHP numeric conversion, errors and evaluation count
--FILE--
<?php
function positive(mixed $value): int|float { return +$value; }
function numericText(string $value): int|float { return +$value; }
function truth(bool $value): int { return +$value; }
function once(int &$calls): string { ++$calls; return '2.5'; }
function main() {
$integer = '42';
$decimal = '1.5';
$boolean = true;
var_dump(+$integer, +$decimal, +$boolean);
$assigned = +$decimal;
var_dump($assigned, numericText('24'), truth(false));
foreach (['12', '1.25', '1e2', '9223372036854775808', true, false, null, 7, -0.0] as $value) {
var_dump(positive($value));
}
foreach (['invalid', [], new stdClass()] as $value) {
try { positive($value); } catch (TypeError $e) { echo $e->getMessage(), "\n"; }
}
set_error_handler(function ($level, $message, $file, $line) { echo $message, "\n"; return true; });
var_dump(numericText('12tail'));
restore_error_handler();
$calls = 0;
var_dump(+once($calls), $calls);
$source = '8';
$alias =& $source;
$result = +$alias;
var_dump($result, $source, $alias);
}
?>
--EXPECT--
int(42)
float(1.5)
int(1)
float(1.5)
int(24)
int(0)
int(12)
float(1.25)
float(100)
float(9.223372036854776E+18)
int(1)
int(0)
int(0)
int(7)
float(-0)
Unsupported operand types: string * int
Unsupported operand types: array * int
Unsupported operand types: stdClass * int
A non-numeric value encountered
int(12)
float(2.5)
int(1)
int(8)
string(1) "8"
string(1) "8"
48 changes: 48 additions & 0 deletions tests/compiler/operator/unary-plus-defaults.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
Unary plus property and parameter defaults use the converted numeric type
--FILE--
<?php
class PositiveDefaults {
public int $text = +'12';
public int $truth = +true;
public int $nothing = +null;
public float $decimal = +'1.5';
public int $minimum = +'-9223372036854775808';
public ?bool $flag = null;
public ?int $optionalInt = null;
public ?float $optionalFloat = null;
public static ?float $optionalStatic = null;
public const VALUE = +'24';
}
function defaultNumber(int $value = +'6'): int { return $value; }
function main() {
$value = new PositiveDefaults();
var_dump($value->text, $value->truth, $value->nothing, $value->decimal);
var_dump(PositiveDefaults::VALUE, defaultNumber());
var_dump($value->minimum === PHP_INT_MIN);
var_dump(+$value->flag);
$value->flag = true;
var_dump(+$value->flag);
var_dump(+$value->optionalInt, +$value->optionalFloat, +PositiveDefaults::$optionalStatic);
var_dump(+(@$value->optionalFloat));
var_dump(+($value->flag ? $value->optionalFloat : PositiveDefaults::$optionalStatic));
$value->optionalFloat = 1.5;
var_dump(+$value->optionalFloat);
}
?>
--EXPECT--
int(12)
int(1)
int(0)
float(1.5)
int(24)
int(6)
bool(true)
int(0)
int(1)
int(0)
int(0)
int(0)
int(0)
int(0)
float(1.5)
17 changes: 17 additions & 0 deletions tests/compiler/operator/unary-plus-new-local.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Unary plus preserves arbitrary precision values assigned to fresh locals
--FILE--
<?php
function main() {
echo (+($integer = std::bigInt(2)))->toString(), "\n";
echo (+($floating = std::bigFloat(2.5)))->toString(), "\n";
echo (+($decimal = std::decimal('2.5')))->toString(), "\n";
var_dump(+($native = 7), +($fraction = 1.5));
}
?>
--EXPECT--
2
2.5
2.5
int(7)
float(1.5)
Loading