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
12 changes: 12 additions & 0 deletions src/CompilerBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3454,6 +3454,15 @@ protected function detectTypeOfExpr($expr): string
) {
return Type::FLOAT;
}
if ($exprType === 'Expr_UnaryMinus') {
// -bool is an int; a non-numeric operand keeps its runtime type.
if ($innerType === Type::BOOL) {
return Type::INT;
}
if (!in_array($innerType, [Type::INT, Type::FLOAT, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
return Type::VAR;
}
}
return $innerType;
case 'Expr_BooleanNot':
case 'Expr_BinaryOp_LogicalAnd':
Expand Down Expand Up @@ -3519,6 +3528,9 @@ protected function detectTypeOfExpr($expr): string
case 'Expr_BinaryOp_BitwiseXor':
$leftType = $this->detectTypeOfExpr($expr->left);
$rightType = $this->detectTypeOfExpr($expr->right);
// PHP converts a bool operand of these operators to int.
$leftType = $leftType === Type::BOOL ? Type::INT : $leftType;
$rightType = $rightType === Type::BOOL ? Type::INT : $rightType;
if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) {
return Type::BIGFLOAT;
}
Expand Down
30 changes: 30 additions & 0 deletions src/Parser/BinaryOpTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,36 @@ protected function parseBinaryOp(NodeAbstract $left, NodeAbstract $right, string
$this->fatalError($left, "Operator '{$op}' is not supported for Big* numeric types");
}

// PHP converts a bool operand of an arithmetic operator to int. Doing it
// here lets the int paths below apply, including the checked varint
// division (a raw C++ division by a false operand is a SIGFPE). % is left
// out: a non-int operand already takes the checked php::fn::mod path.
if (in_array($op, ['+', '-', '*', '/'], true)) {
if ($leftType === Type::BOOL) {
$leftExpr = $this->convertIntExpr($leftExpr, Type::BOOL);
$leftType = Type::INT;
}
if ($rightType === Type::BOOL) {
$rightExpr = $this->convertIntExpr($rightExpr, Type::BOOL);
$rightType = Type::INT;
}
}

// PHP compares a bool with any other scalar as two bools (false < true).
// The raw C++ operator would compare the bool numerically with the
// other operand (false <= -7 is true in PHP, false in C++).
if (in_array($op, ['<', '<=', '>', '>='], true) && ($leftType === Type::BOOL) !== ($rightType === Type::BOOL)) {
$scalars = [Type::INT, Type::FLOAT, Type::STR];
if (in_array($leftType, $scalars, true)) {
$leftExpr = $this->convertBoolExpr($leftExpr, $leftType);
$leftType = Type::BOOL;
}
if (in_array($rightType, $scalars, true)) {
$rightExpr = $this->convertBoolExpr($rightExpr, $rightType);
$rightType = Type::BOOL;
}
}

// Only promote between native types (Int ↔ Float). When one side is
// php::Var, let the Variant operator handle type coercion so that
// run-time PHP type-juggling rules are followed correctly.
Expand Down
9 changes: 9 additions & 0 deletions src/Parser/UnaryExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ protected function parseUnaryMinus(Expr\UnaryMinus $expr): string
}
}
$code = $this->parseExprAsValue($expr->expr);
if ($type === Type::BOOL) {
// PHP negates a bool as an int: -true is -1.
return '-' . $this->convertIntExpr($code, Type::BOOL);
}
if (!in_array($type, [Type::INT, Type::FLOAT], true)) {
// Zend lowers unary minus to multiplication by -1, which applies
// numeric string conversion, warnings and TypeError.
return '(php::Variant(' . $code . ') * -1)';
}

// A bare numeric literal is a single C++ token; negating it directly
// cannot change the parse, and keeps the emitted code (and the test
Expand Down
84 changes: 84 additions & 0 deletions tests/compiler/operator/bool-operands-native.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
--TEST--
A bool operand compares as a bool, negates to an int and is an int in arithmetic (native mode)
--FILE--
<?php
declare(strict_types=1);
function lt(bool $a, int $b): bool { return $a < $b; }
function le(bool $a, int $b): bool { return $a <= $b; }
function gt(int $a, bool $b): bool { return $a > $b; }
function ge(bool $a, float $b): bool { return $a >= $b; }
function lts(bool $a, string $b): bool { return $a < $b; }
function gts(string $a, bool $b): bool { return $a > $b; }
function add(bool $a, int $b): mixed { return $a + $b; }
function mul(bool $a, bool $b): mixed { return $a * $b; }
function sub(float $a, bool $b): mixed { return $a - $b; }
function divb(int $a, bool $b): mixed { return $a / $b; }
function modb(int $a, bool $b): mixed { return $a % $b; }
function negb(bool $a): mixed { return -$a; }
function negs(string $s): mixed { return -$s; }
function negLocal(bool $a): mixed { $x = -$a; return $x; }

function t(string $nome, callable $f): void
{
try {
$v = var_export($f(), true);
echo $nome, ' => ', $v, "\n";
} catch (\Throwable $e) {
echo $nome, ' => ', get_class($e), ': ', $e->getMessage(), "\n";
}
}

function main(): void
{
ini_set('display_errors', '0');
t('false < -7', fn() => lt(false, -7));
t('true < 5', fn() => lt(true, 5));
t('false <= -7', fn() => le(false, -7));
t('true <= 0', fn() => le(true, 0));
t('-7 > false', fn() => gt(-7, false));
t('0 > true', fn() => gt(0, true));
t('true >= 5.5', fn() => ge(true, 5.5));
t('false >= 0.0', fn() => ge(false, 0.0));
t('false < "abc"', fn() => lts(false, 'abc'));
t('true < "0"', fn() => lts(true, '0'));
t('"" > false', fn() => gts('', false));
t('"x" > false', fn() => gts('x', false));
t('true + 41', fn() => add(true, 41));
t('true * true', fn() => mul(true, true));
t('2.5 - true', fn() => sub(2.5, true));
t('7 / true', fn() => divb(7, true));
t('7 % true', fn() => modb(7, true));
t('-true', fn() => negb(true));
t('-false', fn() => negb(false));
t('-"5"', fn() => negs('5'));
t('-"1.5"', fn() => negs('1.5'));
t('-"abc"', fn() => negs('abc'));
t('$x = -true', fn() => negLocal(true));
t('7 % false', fn() => modb(7, false));
}
?>
--EXPECT--
false < -7 => true
true < 5 => false
false <= -7 => true
true <= 0 => false
-7 > false => true
0 > true => false
true >= 5.5 => true
false >= 0.0 => true
false < "abc" => true
true < "0" => false
"" > false => false
"x" > false => true
true + 41 => 42
true * true => 1
2.5 - true => 1.5
7 / true => 7
7 % true => 0
-true => -1
-false => 0
-"5" => -5
-"1.5" => -1.5
-"abc" => TypeError: Unsupported operand types: string * int
$x = -true => -1
7 % false => DivisionByZeroError: Modulo by zero
87 changes: 87 additions & 0 deletions tests/compiler/operator/bool-operands-varint.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--TEST--
A bool operand of arithmetic is an int, so varint division by false raises DivisionByZeroError
--FILE--
<?php
declare(strict_types=1);
use varint_types;
function lt(bool $a, int $b): bool { return $a < $b; }
function le(bool $a, int $b): bool { return $a <= $b; }
function gt(int $a, bool $b): bool { return $a > $b; }
function ge(bool $a, float $b): bool { return $a >= $b; }
function lts(bool $a, string $b): bool { return $a < $b; }
function gts(string $a, bool $b): bool { return $a > $b; }
function add(bool $a, int $b): mixed { return $a + $b; }
function mul(bool $a, bool $b): mixed { return $a * $b; }
function sub(float $a, bool $b): mixed { return $a - $b; }
function divb(int $a, bool $b): mixed { return $a / $b; }
function modb(int $a, bool $b): mixed { return $a % $b; }
function negb(bool $a): mixed { return -$a; }
function negs(string $s): mixed { return -$s; }
function negLocal(bool $a): mixed { $x = -$a; return $x; }

function t(string $nome, callable $f): void
{
try {
$v = var_export($f(), true);
echo $nome, ' => ', $v, "\n";
} catch (\Throwable $e) {
echo $nome, ' => ', get_class($e), ': ', $e->getMessage(), "\n";
}
}

function main(): void
{
ini_set('display_errors', '0');
t('false < -7', fn() => lt(false, -7));
t('true < 5', fn() => lt(true, 5));
t('false <= -7', fn() => le(false, -7));
t('true <= 0', fn() => le(true, 0));
t('-7 > false', fn() => gt(-7, false));
t('0 > true', fn() => gt(0, true));
t('true >= 5.5', fn() => ge(true, 5.5));
t('false >= 0.0', fn() => ge(false, 0.0));
t('false < "abc"', fn() => lts(false, 'abc'));
t('true < "0"', fn() => lts(true, '0'));
t('"" > false', fn() => gts('', false));
t('"x" > false', fn() => gts('x', false));
t('true + 41', fn() => add(true, 41));
t('true * true', fn() => mul(true, true));
t('2.5 - true', fn() => sub(2.5, true));
t('7 / true', fn() => divb(7, true));
t('7 % true', fn() => modb(7, true));
t('-true', fn() => negb(true));
t('-false', fn() => negb(false));
t('-"5"', fn() => negs('5'));
t('-"1.5"', fn() => negs('1.5'));
t('-"abc"', fn() => negs('abc'));
t('$x = -true', fn() => negLocal(true));
t('7 % false', fn() => modb(7, false));
t('7 / false', fn() => divb(7, false));
}
?>
--EXPECT--
false < -7 => true
true < 5 => false
false <= -7 => true
true <= 0 => false
-7 > false => true
0 > true => false
true >= 5.5 => true
false >= 0.0 => true
false < "abc" => true
true < "0" => false
"" > false => false
"x" > false => true
true + 41 => 42
true * true => 1
2.5 - true => 1.5
7 / true => 7
7 % true => 0
-true => -1
-false => 0
-"5" => -5
-"1.5" => -1.5
-"abc" => TypeError: Unsupported operand types: string * int
$x = -true => -1
7 % false => DivisionByZeroError: Modulo by zero
7 / false => DivisionByZeroError: Division by zero
Loading