From 27b58f027f65494480cdb40b3b4f2317ae0625d1 Mon Sep 17 00:00:00 2001 From: Giandonn Date: Fri, 2 Oct 2026 22:12:32 +0000 Subject: [PATCH] fix(compiler): PHP semantics for bool operands of comparisons, arithmetic and unary minus Native bool values reached raw C++ operators that treat them as the integers 0 and 1, which differs from PHP in three places: - Relational comparison. PHP compares a bool with any other scalar as two bools (false < true); the C++ operator compared numerically: function le(bool $a, int $b): bool { return $a <= $b; } le(false, -7); // PHP: true compiled: false <, <=, > and >= now convert the non-bool scalar operand to bool. == and <=> already used php::equals()/php::compare(). - Arithmetic. A bool operand of +, -, * or / was not treated as an int, so the checked varint paths did not apply and `7 / false` divided by a raw bool: a SIGFPE even with `use varint_types`, where PHP raises DivisionByZeroError. Bool operands are now converted to int first. % is unchanged: a non-int operand already takes php::fn::mod. - Unary minus. -$bool stayed a bool (-true printed `true`, PHP gives -1). It is now an int, and a non-numeric operand (e.g. a string) is lowered like Zend does, as multiplication by -1, matching what unary plus already does. The inferred types follow (bitwise/shift/arithmetic operators treat a bool operand as int; -bool is int; minus of a non-numeric value is dynamic). --- src/CompilerBase.php | 12 +++ src/Parser/BinaryOpTrait.php | 30 +++++++ src/Parser/UnaryExpressionTrait.php | 9 ++ .../operator/bool-operands-native.phpt | 84 ++++++++++++++++++ .../operator/bool-operands-varint.phpt | 87 +++++++++++++++++++ 5 files changed, 222 insertions(+) create mode 100755 tests/compiler/operator/bool-operands-native.phpt create mode 100755 tests/compiler/operator/bool-operands-varint.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index db84d361..64e77e4c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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': @@ -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; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index fed76ccb..fa7001fa 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -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. diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 07e6a1ef..c57d1915 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -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 diff --git a/tests/compiler/operator/bool-operands-native.phpt b/tests/compiler/operator/bool-operands-native.phpt new file mode 100755 index 00000000..939fa3e3 --- /dev/null +++ b/tests/compiler/operator/bool-operands-native.phpt @@ -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-- + $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 diff --git a/tests/compiler/operator/bool-operands-varint.phpt b/tests/compiler/operator/bool-operands-varint.phpt new file mode 100755 index 00000000..b7cb28eb --- /dev/null +++ b/tests/compiler/operator/bool-operands-varint.phpt @@ -0,0 +1,87 @@ +--TEST-- +A bool operand of arithmetic is an int, so varint division by false raises DivisionByZeroError +--FILE-- + $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