diff --git a/phpunit/code/unary-plus-codegen.php b/phpunit/code/unary-plus-codegen.php new file mode 100644 index 00000000..f9d89902 --- /dev/null +++ b/phpunit/code/unary-plus-codegen.php @@ -0,0 +1,11 @@ +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); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index a9f95fd1..79aef34b 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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 diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 6fbe1f62..07e6a1ef 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -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)) { @@ -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)'; } } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 5241d60d..cfafe630 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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())) { diff --git a/tests/compiler/operator/unary-plus-assignment.phpt b/tests/compiler/operator/unary-plus-assignment.phpt new file mode 100644 index 00000000..abb4a90c --- /dev/null +++ b/tests/compiler/operator/unary-plus-assignment.phpt @@ -0,0 +1,40 @@ +--TEST-- +Unary plus uses the converted assignment result for typed properties +--FILE-- +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) diff --git a/tests/compiler/operator/unary-plus-conversion.phpt b/tests/compiler/operator/unary-plus-conversion.phpt new file mode 100644 index 00000000..9536236f --- /dev/null +++ b/tests/compiler/operator/unary-plus-conversion.phpt @@ -0,0 +1,58 @@ +--TEST-- +Unary plus preserves PHP numeric conversion, errors and evaluation count +--FILE-- +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" diff --git a/tests/compiler/operator/unary-plus-defaults.phpt b/tests/compiler/operator/unary-plus-defaults.phpt new file mode 100644 index 00000000..acf14065 --- /dev/null +++ b/tests/compiler/operator/unary-plus-defaults.phpt @@ -0,0 +1,48 @@ +--TEST-- +Unary plus property and parameter defaults use the converted numeric type +--FILE-- +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) diff --git a/tests/compiler/operator/unary-plus-new-local.phpt b/tests/compiler/operator/unary-plus-new-local.phpt new file mode 100644 index 00000000..31b4e8da --- /dev/null +++ b/tests/compiler/operator/unary-plus-new-local.phpt @@ -0,0 +1,17 @@ +--TEST-- +Unary plus preserves arbitrary precision values assigned to fresh locals +--FILE-- +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)