fix(compiler): apply unary plus numeric conversion (#132)

* fix(compiler): apply unary plus numeric conversion

* fix(compiler): infer unary plus assignments from target type

* fix(compiler): retain unary plus inference for fresh locals

* fix(compiler): resolve existing unary assignment locals by compiled name
master
yavon007 2 weeks ago committed by GitHub
parent 4f61dda6c3
commit c54b590ca2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      phpunit/code/unary-plus-codegen.php
  2. 24
      phpunit/src/UnaryPlusCodegenTest.php
  3. 12
      src/CompilerBase.php
  4. 68
      src/Parser/UnaryExpressionTrait.php
  5. 1
      src/Preprocessor.php
  6. 40
      tests/compiler/operator/unary-plus-assignment.phpt
  7. 58
      tests/compiler/operator/unary-plus-conversion.phpt
  8. 48
      tests/compiler/operator/unary-plus-defaults.phpt
  9. 17
      tests/compiler/operator/unary-plus-new-local.phpt

@ -0,0 +1,11 @@
<?php
function positiveInteger(int $number): int
{
return +$number;
}
function positiveFloat(float $number): float
{
return +$number;
}

@ -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);
}
}

@ -3369,8 +3369,18 @@ class CompilerBase implements PropertyAccessContext
$exprType = $expr->getType(); $exprType = $expr->getType();
switch ($exprType) { switch ($exprType) {
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus': 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); $innerType = $this->detectTypeOfExpr($expr->expr);
if ( if (
$this->varIntTypes $this->varIntTypes

@ -14,6 +14,56 @@ use TypePhp\Type;
trait UnaryExpressionTrait 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 protected function parseCastVoid(Expr\Cast\Void_ $node): string
{ {
if (!$node->getAttribute(VoidCastValidationVisitor::ALLOWED_ATTRIBUTE, false)) { if (!$node->getAttribute(VoidCastValidationVisitor::ALLOWED_ATTRIBUTE, false)) {
@ -148,6 +198,22 @@ trait UnaryExpressionTrait
} }
$this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '+', true); $this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '+', true);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand'); $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)';
} }
} }

@ -2594,7 +2594,6 @@ class Preprocessor extends CompilerBase
case 'Expr_Array': case 'Expr_Array':
return 'array'; return 'array';
case 'Expr_UnaryMinus': case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1); return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch': case 'Expr_ConstFetch':
return match (strtolower($node->name->toString())) { return match (strtolower($node->name->toString())) {

@ -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)

@ -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"

@ -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)

@ -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…
Cancel
Save