feat(compiler): enhance big number type handling in logical operations

- Add detectTypeOfExpr support for unary and binary logical operators
- Implement proper bool conversion for big integer, float and decimal types
- Update convertBoolExpr to skip conversion when type is already BOOL
- Add convertConditionExpr method to handle big number truth values
- Modify empty() implementation to use numeric truth values for big numbers
- Enhance ternary operator type detection with conditional logic
- Update loop control parsing to use condition expression conversion
- Add proper type assertions for XOR logical operations
- Remove redundant isValidDefineName tests from TraitsTest
- Simplify ConstantExpressionValidationVisitor initialization
- Initialize cValue variable in gen_stub.php for constant assertions
- Add comprehensive tests for big number logical and unary operators
- Add operator error boundary tests with PHP-compatible exception types
- Add reverse and compound operator tests for big numeric types
pull/44/head
韩天峰 3 weeks ago
parent f38b6cf0c2
commit 21d0224769
  1. 23
      phpunit/src/TraitsTest.php
  2. 28
      src/CompilerBase.php
  3. 16
      src/Parser/BinaryOpTrait.php
  4. 14
      src/Parser/LoopControlTrait.php
  5. 7
      src/Parser/SelectionExpressionTrait.php
  6. 12
      src/Parser/TypeConversionTrait.php
  7. 5
      src/Parser/UnaryExpressionTrait.php
  8. 5
      src/Preprocessor.php
  9. 2
      src/gen_stub.php
  10. 70
      tests/compiler/bignumber/logical-and-unary-operators.phpt
  11. 52
      tests/compiler/bignumber/operator-error-boundaries.phpt
  12. 42
      tests/compiler/bignumber/reverse-and-compound-operators.phpt

@ -95,29 +95,6 @@ class TraitsTest extends TestCase
$this->assertFalse($this->invoke('checkArgType', 'void', 'php::Str'));
}
// ========================================================================
// FuncCallOptimizer::isValidDefineName
// ========================================================================
public function testIsValidDefineNameValid(): void
{
$this->assertTrue($this->invoke('isValidDefineName', 'MY_CONSTANT'));
$this->assertTrue($this->invoke('isValidDefineName', 'APP_NAME'));
$this->assertTrue($this->invoke('isValidDefineName', '_PRIVATE'));
$this->assertTrue($this->invoke('isValidDefineName', 'camelCase'));
$this->assertTrue($this->invoke('isValidDefineName', 'Test123'));
$this->assertTrue($this->invoke('isValidDefineName', '_'));
}
public function testIsValidDefineNameInvalid(): void
{
$this->assertFalse($this->invoke('isValidDefineName', '123abc')); // starts with digit
$this->assertFalse($this->invoke('isValidDefineName', 'has space')); // contains space
$this->assertFalse($this->invoke('isValidDefineName', 'has-dash')); // contains dash
$this->assertFalse($this->invoke('isValidDefineName', '')); // empty
$this->assertFalse($this->invoke('isValidDefineName', '0abc')); // starts with zero
}
// ========================================================================
// StdContainerParser::isStdContainerType
// ========================================================================

@ -1516,6 +1516,7 @@ class CompilerBase implements PropertyAccessContext
if ($cond instanceof Expr\Assign) {
$condExpr = '(' . $condExpr . ')';
}
$condExpr = $this->convertConditionExpr($cond, $condExpr);
$code .= $openPrefix . '(' . $condExpr . ') {' . PHP_EOL;
return $code;
}
@ -2513,7 +2514,23 @@ class CompilerBase implements PropertyAccessContext
$exprType = $expr->getType();
switch ($exprType) {
case 'Expr_UnaryMinus':
case 'Expr_UnaryPlus':
return $this->detectTypeOfExpr($expr->expr);
case 'Expr_BooleanNot':
case 'Expr_BinaryOp_LogicalAnd':
case 'Expr_BinaryOp_BooleanAnd':
case 'Expr_BinaryOp_LogicalOr':
case 'Expr_BinaryOp_BooleanOr':
case 'Expr_BinaryOp_LogicalXor':
case 'Expr_BinaryOp_Equal':
case 'Expr_BinaryOp_NotEqual':
case 'Expr_BinaryOp_Identical':
case 'Expr_BinaryOp_NotIdentical':
case 'Expr_BinaryOp_Smaller':
case 'Expr_BinaryOp_SmallerOrEqual':
case 'Expr_BinaryOp_Greater':
case 'Expr_BinaryOp_GreaterOrEqual':
return Type::BOOL;
case 'Expr_BitwiseNot':
$inner = $this->detectTypeOfExpr($expr->expr);
return $inner === Type::BIGINT ? Type::BIGINT : Type::INT;
@ -2542,6 +2559,12 @@ class CompilerBase implements PropertyAccessContext
case 'Expr_BinaryOp_Concat':
case 'Expr_AssignOp_Concat':
return Type::STR;
case 'Expr_Ternary':
$ifType = $expr->if === null
? $this->detectTypeOfExpr($expr->cond)
: $this->detectTypeOfExpr($expr->if);
$elseType = $this->detectTypeOfExpr($expr->else);
return $ifType === $elseType ? $ifType : Type::VAR;
case 'Expr_BinaryOp_Plus':
case 'Expr_BinaryOp_Minus':
case 'Expr_BinaryOp_Mul':
@ -2553,7 +2576,6 @@ class CompilerBase implements PropertyAccessContext
case 'Expr_BinaryOp_BitwiseAnd':
case 'Expr_BinaryOp_BitwiseOr':
case 'Expr_BinaryOp_BitwiseXor':
case 'Expr_BinaryOp_BooleanAnd':
$leftType = $this->detectTypeOfExpr($expr->left);
$rightType = $this->detectTypeOfExpr($expr->right);
if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) {
@ -3449,6 +3471,10 @@ class CompilerBase implements PropertyAccessContext
protected function parseEmpty(Expr\Empty_ $expr): string
{
$type = $this->detectTypeOfExpr($expr->expr);
if (in_array($type, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
return '!(' . $this->convertBoolExpr($this->parseExprAsValue($expr->expr), $type) . ')';
}
return $this->parseChainedExpr($expr->expr, self::OP_EMPTY);
}

@ -474,9 +474,10 @@ trait BinaryOpTrait
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount);
$this->checkVarMustExist($right, $rightExpr);
$leftBool = $this->convertBoolExpr((string) $leftExpr);
$leftBool = $this->convertBoolExpr((string) $leftExpr, $this->detectTypeOfExpr($left));
$rightBool = $this->convertBoolExpr((string) $rightExpr, $this->detectTypeOfExpr($right));
if (!$rightBeforeStmts && !$rightAfterStmts) {
return '(' . $leftBool . ' ' . $op . ' ' . $this->convertBoolExpr((string) $rightExpr) . ')';
return '(' . $leftBool . ' ' . $op . ' ' . $rightBool . ')';
}
$shortCircuitValue = $op === '&&' ? 'false' : 'true';
@ -490,8 +491,9 @@ trait BinaryOpTrait
$code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';';
$code .= $this->formatCapturedStmtLines($rightAfterStmts);
$rightExpr = $rightTmpVar;
$rightBool = $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right));
}
$code .= $this->getIndent() . 'return ' . $this->convertBoolExpr((string) $rightExpr) . ';';
$code .= $this->getIndent() . 'return ' . $rightBool . ';';
$code .= $this->getIndent() . '}';
$code .= $this->getIndent() . 'return ' . $shortCircuitValue . ';';
$code .= $this->getIndent() . '}()';
@ -501,7 +503,13 @@ trait BinaryOpTrait
protected function parseBinaryOpLogicalXor(Expr\BinaryOp\LogicalXor $expr): string
{
return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '^'));
$this->assertExprCanBeUsedAsCondition($expr->left, 'logical operand');
$this->assertExprCanBeUsedAsCondition($expr->right, 'logical operand');
$left = $this->parseOrderedBinaryOperand($expr->left);
$right = $this->parseOrderedBinaryOperand($expr->right);
$leftBool = $this->convertBoolExpr($left, $this->detectTypeOfExpr($expr->left));
$rightBool = $this->convertBoolExpr($right, $this->detectTypeOfExpr($expr->right));
return '(' . $leftBool . ' != ' . $rightBool . ')';
}
protected function parseBinaryOpSmallerOrEqual(Expr\BinaryOp\SmallerOrEqual $expr): string

@ -42,8 +42,8 @@ trait LoopControlTrait
[$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$condExpr = $this->stringifyParsedExpr($condExpr);
$hasCondStmts = $hasCondStmts || $beforeStmts || $afterStmts;
$list_cond[] = [$condExpr, $beforeStmts, $afterStmts];
$list_cond_expr[] = $condExpr;
$list_cond[] = [$expr, $condExpr, $beforeStmts, $afterStmts];
$list_cond_expr[] = $this->convertConditionExpr($expr, $condExpr);
}
$code .= $this->parseBeforeStmtLines() . PHP_EOL;
@ -55,7 +55,7 @@ trait LoopControlTrait
} else {
$condResult = $this->genTmpVarName();
$condCode .= $this->getIndent() . 'bool ' . $condResult . ' = true;' . PHP_EOL;
foreach ($list_cond as [$condExpr, $beforeStmts, $afterStmts]) {
foreach ($list_cond as [$condNode, $condExpr, $beforeStmts, $afterStmts]) {
$condCode .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
@ -63,7 +63,7 @@ trait LoopControlTrait
$condCode .= $this->formatCapturedStmtLines($afterStmts);
$condExpr = $tmpVar;
}
$condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertBoolExpr($condExpr) . ';' . PHP_EOL;
$condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertConditionExpr($condNode, $condExpr) . ';' . PHP_EOL;
}
$condCode .= $this->getIndent() . 'return ' . $condResult . ';';
}
@ -122,8 +122,10 @@ trait LoopControlTrait
$code .= $this->formatCapturedStmtLines($afterStmts);
$cond = $tmpVar;
}
$cond = $this->convertConditionExpr($v->cond, $cond);
$code .= $this->getIndent() . 'if (!(' . $cond . ')) { break; }' . PHP_EOL;
} else {
$cond = $this->convertConditionExpr($v->cond, $cond);
$code .= 'while (' . $cond . ') {' . PHP_EOL;
}
$code .= $this->parseBlockStmts($stmts);
@ -148,9 +150,11 @@ trait LoopControlTrait
$condCode .= $this->formatCapturedStmtLines($afterStmts);
$cond = $tmpVar;
}
$condCode .= $this->getIndent() . 'return ' . $this->convertBoolExpr($cond) . ';';
$condCode .= $this->getIndent() . 'return ' . $this->convertConditionExpr($v->cond, $cond) . ';';
$condCode .= $this->getIndent() . '}()';
$cond = $condCode;
} else {
$cond = $this->convertConditionExpr($v->cond, $cond);
}
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'do {' . PHP_EOL;

@ -54,6 +54,7 @@ trait SelectionExpressionTrait
$code .= $this->formatCapturedStmtLines($condAfterStmts);
$cond = $condTmpVar;
}
$cond = $this->convertConditionExpr($expr->cond, $cond);
$code .= $this->getIndent() . 'if (' . $cond . ') {';
$code .= $this->formatTernaryReturn($if, $ifBeforeStmts, $ifAfterStmts);
$code .= $this->getIndent() . '} else {';
@ -62,6 +63,7 @@ trait SelectionExpressionTrait
$code .= $this->getIndent() . '}()';
return $code;
}
$cond = $this->convertConditionExpr($expr->cond, $cond);
return '(' . $cond . ') ? (' . $if . ') : (' . $else . ')';
}
@ -167,6 +169,11 @@ trait SelectionExpressionTrait
if ($chainOpResult) {
$leftExpr = $chainOpResult;
}
$leftType = $this->detectTypeOfExpr($left);
if ($op === self::OP_NOT_EMPTY
&& in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
$condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')';
}
$rightBeforeStmtCount = count($this->context->beforeStmtLines);
$rightAfterStmtCount = count($this->context->afterStmtLines);

@ -162,6 +162,9 @@ trait TypeConversionTrait
protected function convertBoolExpr(string $expr, string $fromType = ''): string
{
if ($fromType === Type::BOOL) {
return $expr;
}
$bigConversion = match ($fromType) {
Type::BIGINT => 'php::BigInt::toBool',
Type::BIGFLOAT => 'php::BigFloat::toBool',
@ -178,6 +181,15 @@ trait TypeConversionTrait
return $expr;
}
protected function convertConditionExpr(NodeAbstract $node, string $expr): string
{
$type = $this->detectTypeOfExpr($node);
if (in_array($type, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) {
return $this->convertBoolExpr($expr, $type);
}
return $expr;
}
protected function convertExprType(string $expr, $leftType, $rightType): string
{
if ($leftType === Type::FLOAT or $rightType === Type::FLOAT) {

@ -28,7 +28,10 @@ trait UnaryExpressionTrait
protected function parseBooleanNot(Expr\BooleanNot $expr): string
{
$this->assertExprCanBeUsedAsCondition($expr->expr, 'boolean operand');
return '!(' . $this->parseExprAsValue($expr->expr) . ')';
return '!(' . $this->convertBoolExpr(
$this->parseExprAsValue($expr->expr),
$this->detectTypeOfExpr($expr->expr)
) . ')';
}
protected function parseCastInt(Expr\Cast\Int_ $node): string

@ -149,10 +149,7 @@ class Preprocessor extends CompilerBase
fn (Node $node, string $message) => $this->warning($node, $message),
$this->file,
));
$traverser->addVisitor(new ConstantExpressionValidationVisitor(
$this->phpVersion,
fn (Node $node, string $message) => $this->fatalError($node, $message),
));
$traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion));
$traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file));
$stmts = $traverser->traverse($ast);

@ -3007,6 +3007,8 @@ class ConstInfo extends VariableLike
return "\tZEND_ASSERT($cExpr == NULL);\n";
}
$cValue = null;
if ($value->type->isBool()) {
$cValue = $constValue ? "true" : "false";
return "\tZEND_ASSERT($cExpr == $cValue);\n";

@ -0,0 +1,70 @@
--TEST--
Big numeric unary plus and boolean contexts use numeric truth values
--FILE--
<?php
declare(strict_types=1);
use native_types;
function main(): void {
$bi0 = std::bigInt(0);
$bi1 = std::bigInt(1);
echo (+$bi1)->toString(), "\n";
var_dump(!$bi0, !$bi1);
var_dump($bi0 && $bi1, $bi0 || $bi1, ($bi1 xor $bi1), ($bi0 xor $bi1));
$dec0 = std::decimal("0.0");
$dec1 = std::decimal("1.0");
echo (+$dec1)->toString(), "\n";
var_dump(!$dec0, !$dec1);
var_dump($dec0 && $dec1, $dec0 || $dec1, ($dec1 xor $dec1), ($dec0 xor $dec1));
$bf0 = std::bigFloat("0");
$bf1 = std::bigFloat("1");
echo (+$bf1)->toString(), "\n";
var_dump(!$bf0, !$bf1);
var_dump($bf0 && $bf1, $bf0 || $bf1, ($bf1 xor $bf1), ($bf0 xor $bf1));
echo $bi0 ? "bad-if\n" : "if-ok\n";
echo $dec0 ? "bad-ternary\n" : "ternary-ok\n";
echo ($bf0 ?: std::bigFloat("9"))->toString(), "\n";
var_dump(empty($bi0), empty($bi1));
while ($bi0) {
echo "bad-while\n";
}
for (; $dec0;) {
echo "bad-for\n";
}
do {
echo "do-once\n";
} while ($bf0);
}
?>
--EXPECT--
1
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
bool(true)
1.0
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
bool(true)
1
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
bool(true)
if-ok
ternary-ok
9
bool(true)
bool(false)
do-once

@ -0,0 +1,52 @@
--TEST--
Big numeric operator error boundaries use PHP-compatible exception types
--FILE--
<?php
declare(strict_types=1);
use native_types;
function main(): void {
try {
$unused = std::bigInt(1) / 0;
} catch (DivisionByZeroError $e) {
echo "bigint division by zero\n";
}
try {
$unused = std::bigInt(1) % 0;
} catch (DivisionByZeroError $e) {
echo "bigint modulo by zero\n";
}
try {
$unused = std::bigInt(2) ** -1;
} catch (TypeError $e) {
echo "bigint negative exponent\n";
}
try {
$unused = std::decimal("1") % 0;
} catch (DivisionByZeroError $e) {
echo "decimal modulo by zero\n";
}
try {
$unused = std::decimal("NaN") <=> std::decimal("1");
} catch (ArithmeticError $e) {
echo "decimal NaN comparison\n";
}
try {
$unused = std::bigFloat("NAN") <=> std::bigFloat("1");
} catch (ArithmeticError $e) {
echo "bigfloat NaN comparison\n";
}
}
?>
--EXPECT--
bigint division by zero
bigint modulo by zero
bigint negative exponent
decimal modulo by zero
decimal NaN comparison
bigfloat NaN comparison

@ -0,0 +1,42 @@
--TEST--
Big numeric reverse non-commutative and BigFloat compound operators
--FILE--
<?php
declare(strict_types=1);
use native_types;
function main(): void {
$bi = std::bigInt(4);
echo (10 - $bi)->toString(), "\n";
echo (10 / $bi)->toString(), "\n";
echo (10 % $bi)->toString(), "\n";
echo (std::bigInt(-7) / 3)->toString(), "\n";
echo (std::bigInt(-7) % 3)->toString(), "\n";
$dec = std::decimal("4.0");
echo (10 - $dec)->toString(), "\n";
echo (10 / $dec)->toString(), "\n";
echo (10 % $dec)->toString(), "\n";
$bf = std::bigFloat("4.0");
echo (10 - $bf)->toString(), "\n";
echo (10 / $bf)->toString(), "\n";
$bf += 2;
$bf -= 1;
$bf *= 3;
$bf /= 5;
echo $bf->toString(), "\n";
}
?>
--EXPECT--
6
2
2
-2
-1
6.0
2.5
2.0
6
2.5
3
Loading…
Cancel
Save