assertExprCanBeUsedAsValue($left, 'binary operand'); $this->assertExprCanBeUsedAsValue($right, 'binary operand'); // 运算逻辑,优先转为数字 $leftExpr = $this->parseOrderedBinaryOperand($left); $rightExpr = $this->parseOrderedBinaryOperand($right); $this->checkVarMustExist($left, $leftExpr); $this->checkVarMustExist($right, $rightExpr); $leftType = $this->detectTypeOfExpr($left); $rightType = $this->detectTypeOfExpr($right); if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) { // BigFloat cannot implicitly mix with BigInt or Decimal — risk of precision loss if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) { $this->fatalError($left, 'Cannot mix BigFloat and BigInt implicitly. Use std::bigFloat() to convert explicitly.'); } if ($leftType === Type::DECIMAL || $rightType === Type::DECIMAL) { $this->fatalError($left, 'Cannot mix BigFloat and Decimal implicitly. Use std::bigFloat() to convert explicitly.'); } if ($leftType !== Type::BIGFLOAT) { $leftExpr = $this->convertBigFloatExpr($leftExpr, $leftType); } if ($rightType !== Type::BIGFLOAT) { $rightExpr = $this->convertBigFloatExpr($rightExpr, $rightType); } $arithOpMap = ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div']; $method = $arithOpMap[$op] ?? null; if ($method) { return 'php::BigFloat::' . $method . '(' . $leftExpr . ', ' . $rightExpr . ')'; } $cmpOpMap = ['<' => '< 0', '>' => '> 0', '<=' => '<= 0', '>=' => '>= 0']; if (isset($cmpOpMap[$op])) { return 'php::toBool(php::BigFloat::cmp(' . $leftExpr . ', ' . $rightExpr . ') ' . $cmpOpMap[$op] . ')'; } } if ($leftType === Type::DECIMAL || $rightType === Type::DECIMAL) { // BigInt and Decimal cannot implicitly mix — risk of precision loss if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) { $this->fatalError($left, 'Cannot mix BigInt and Decimal implicitly. Use std::decimal() or std::bigInt() to convert explicitly.'); } if ($leftType !== Type::DECIMAL) { $leftExpr = $this->convertDecimalExpr($leftExpr, $leftType, $left); } if ($rightType !== Type::DECIMAL) { $rightExpr = $this->convertDecimalExpr($rightExpr, $rightType, $right); } $arithOpMap = ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod']; $method = $arithOpMap[$op] ?? null; if ($method) { return 'php::Decimal::' . $method . '(' . $leftExpr . ', ' . $rightExpr . ')'; } $cmpOpMap = ['<' => '< 0', '>' => '> 0', '<=' => '<= 0', '>=' => '>= 0']; if (isset($cmpOpMap[$op])) { return 'php::toBool(php::Decimal::cmp(' . $leftExpr . ', ' . $rightExpr . ') ' . $cmpOpMap[$op] . ')'; } } if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) { // Bitwise shifts: right operand is shift amount, must stay as Int if ($op === '<<' || $op === '>>') { if ($leftType !== Type::BIGINT) { $leftExpr = $this->convertBigIntExpr($leftExpr, $leftType); } if ($rightType === Type::BIGINT) { $rightExpr = 'php::BigInt::toInt(' . $rightExpr . ')'; } elseif ($rightType !== Type::INT) { $rightExpr = $this->convertExprType($rightExpr, $rightType, Type::INT); } $method = ($op === '<<') ? 'bitShiftLeft' : 'bitShiftRight'; return 'php::BigInt::' . $method . '(' . $leftExpr . ', ' . $rightExpr . ')'; } if ($leftType !== Type::BIGINT) { $leftExpr = $this->convertBigIntExpr($leftExpr, $leftType); } if ($rightType !== Type::BIGINT) { $rightExpr = $this->convertBigIntExpr($rightExpr, $rightType); } $arithOpMap = ['+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '&' => 'bitAnd', '|' => 'bitOr', '^' => 'bitXor']; $method = $arithOpMap[$op] ?? null; if ($method) { return 'php::BigInt::' . $method . '(' . $leftExpr . ', ' . $rightExpr . ')'; } $cmpOpMap = ['<' => '< 0', '>' => '> 0', '<=' => '<= 0', '>=' => '>= 0']; if (isset($cmpOpMap[$op])) { return 'php::toBool(php::BigInt::cmp(' . $leftExpr . ', ' . $rightExpr . ') ' . $cmpOpMap[$op] . ')'; } } // Any Big*-typed operand reaching here means no Big* block handled the operator $bigTypes = [Type::BIGFLOAT, Type::DECIMAL, Type::BIGINT]; if (in_array($leftType, $bigTypes, true) || in_array($rightType, $bigTypes, true)) { $this->fatalError($left, "Operator '{$op}' is not supported for Big* numeric types"); } // 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. if ($leftType === Type::FLOAT && $rightType === Type::INT) { $rightExpr = $this->convertExprType($rightExpr, Type::FLOAT, $rightType); } elseif ($rightType === Type::FLOAT && $leftType === Type::INT) { $leftExpr = $this->convertExprType($leftExpr, $leftType, Type::FLOAT); } $this->guardLiteralDivisionByZero($right, $op); $constantDivisionByZero = $this->handleNestedConstantDivisionByZero( $right, $op, $leftExpr, $rightExpr ); if ($constantDivisionByZero !== null) { return $constantDivisionByZero; } if ($op === '%' and !($leftType === Type::INT and $rightType === Type::INT)) { return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; } if ($op === '<<' || $op === '>>') { $foldedShift = $this->tryFoldConstantShift($left, $right, $op, $leftExpr, $rightExpr); if ($foldedShift !== null) { return $foldedShift; } } $folded = $this->tryFoldConstantIntArithmetic($left, $right, $op); if ($folded !== null) { return $folded; } return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))'; } /** * Fold constant integer shifts to PHP semantics in non-native mode. * * PHP shifts by >= word size to 0 (left) or -1/0 (right, arithmetic), and * throws a catchable ArithmeticError for negative shift counts. Native C++ * shifts are undefined for those counts, so the constant case is folded * (>= word size) or routed through php::Var (negative, so the Zend shift * function raises the catchable error at runtime). */ protected function tryFoldConstantShift( NodeAbstract $left, NodeAbstract $right, string $op, string $leftExpr, string $rightExpr ): ?string { $leftValue = $this->constantIntValue($left); $shiftValue = $this->constantIntValue($right); if ($leftValue === null || $shiftValue === null) { return null; } $wordSize = PHP_INT_SIZE * 8; if ($this->nativeTypes) { if ($shiftValue >= $wordSize) { $this->fatalError( $right, 'Bit shift count ' . $shiftValue . ' is >= ' . $wordSize . ' and is not supported in native mode' ); } if ($shiftValue < 0) { $this->fatalError( $right, 'Bit shift by a negative number is not supported in native mode' ); } if ($op === '>>' && $leftValue < 0) { $this->fatalError( $left, 'Right shift of a negative value is implementation-defined in C++' . ' and is not supported in native mode' ); } if ($op === '<<' && $leftValue < 0) { $this->fatalError( $left, 'Left shift of a negative value is undefined behavior in C++' . ' and is not supported in native mode' ); } if ($op === '<<' && $this->leftShiftTouchesSignBit($leftValue, $shiftValue)) { $this->fatalError( $left, 'Left shift that changes the sign bit is undefined behavior in C++' . ' and is not supported in native mode' ); } return null; } if ($shiftValue >= $wordSize) { $result = $op === '<<' ? '0LL' : ($leftValue < 0 ? '-1LL' : '0LL'); $this->warning( $right, 'Bit shift count ' . $shiftValue . ' is >= ' . $wordSize . '; folding with PHP semantics (left shift to 0, right shift to -1 for negative operands, 0 otherwise)' ); return $result; } if ($shiftValue < 0) { $this->warning( $right, 'Bit shift by a negative number throws ArithmeticError at runtime' ); // Route through php::Var so the Zend shift function raises the catchable error. return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))'; } return null; } /** * Whether a constant left shift of a non-negative value would set the sign * bit (overflow the signed range), which is undefined behavior in C++. */ protected function leftShiftTouchesSignBit(int $value, int $shift): bool { if ($value < 0 || $shift <= 0) { return false; } if ($shift >= PHP_INT_SIZE * 8) { return true; } // Avoid performing the overflowing shift while checking it. Testing // the wrapped PHP result misses cases such as 2 << 63, which becomes // zero in PHP but is undefined/implementation-defined in C++17. return $value > (PHP_INT_MAX >> $shift); } /** * Fold constant int arithmetic that cannot be emitted as a plain C++ * signed-integer expression. * * PHP promotes overflowing arithmetic to float. It also defines * PHP_INT_MIN % -1 as zero, while the equivalent C++ remainder expression * has undefined behavior. Native mode rejects every statically detectable * undefined operation instead of relying on compiler-specific behavior. */ protected function tryFoldConstantIntArithmetic(NodeAbstract $left, NodeAbstract $right, string $op): ?string { $evaluation = $this->evaluateConstantIntArithmetic($left, $right, $op); if ($evaluation === null) { return null; } if ($this->nativeTypes) { if ($evaluation['cppUndefined']) { $this->fatalError( $left, 'Constant integer operation ' . $evaluation['left'] . ' ' . $op . ' ' . $evaluation['right'] . ' has undefined behavior in C++ native mode' ); } return null; } if ($op === '%' && $evaluation['cppUndefined']) { return $this->genIntegerLiteral($evaluation['result']); } if (is_int($evaluation['result'])) { return null; } if ($evaluation['cppUndefined']) { $this->warning( $left, 'Constant integer arithmetic overflows int64; folding to PHP float result (' . $evaluation['left'] . ' ' . $op . ' ' . $evaluation['right'] . ')' ); } return $this->genFloatLiteral($evaluation['result']); } /** * @return array{left: int, right: int, result: int|float, cppUndefined: bool}|null */ protected function evaluateConstantIntArithmetic( NodeAbstract $left, NodeAbstract $right, string $op ): ?array { if (!in_array($op, ['+', '-', '*', '/', '%'], true)) { return null; } $leftValue = $this->constantIntValue($left); $rightValue = $this->constantIntValue($right); if ($leftValue === null || $rightValue === null) { return null; } if (($op === '/' || $op === '%') && $rightValue === 0) { // Division by zero is rejected by guardLiteralDivisionByZero. return null; } $result = match ($op) { '+' => $leftValue + $rightValue, '-' => $leftValue - $rightValue, '*' => $leftValue * $rightValue, '/' => $leftValue / $rightValue, '%' => $leftValue % $rightValue, }; $cppUndefined = match ($op) { '+', '-', '*' => is_float($result), '/', '%' => $leftValue === PHP_INT_MIN && $rightValue === -1, }; return [ 'left' => $leftValue, 'right' => $rightValue, 'result' => $result, 'cppUndefined' => $cppUndefined, ]; } /** * Resolve a compile-time integer constant value, or null when the * expression is not a statically known int constant. */ protected function constantIntValue(NodeAbstract $expr): ?int { $value = $this->constantNumericValue($expr, $this->nativeTypes); return is_int($value) ? $value : null; } /** * Evaluate a numeric literal tree without touching dynamic expressions. * Native mode uses C++17 integer-division semantics so an enclosing * operation can still be checked for undefined behavior. */ protected function constantNumericValue(NodeAbstract $expr, bool $nativeSemantics): int|float|null { if ($expr instanceof Node\Scalar\Int_) { return $expr->value; } if ($expr instanceof Node\Scalar\Float_) { return $expr->value; } if ($expr instanceof Node\Expr\UnaryPlus) { return $this->constantNumericValue($expr->expr, $nativeSemantics); } if ($expr instanceof Node\Expr\UnaryMinus) { $value = $this->constantNumericValue($expr->expr, $nativeSemantics); return $value === null ? null : -$value; } if ($expr instanceof Node\Expr\ConstFetch) { $name = strtolower($expr->name->toString()); return match ($name) { 'php_int_max' => PHP_INT_MAX, 'php_int_min' => PHP_INT_MIN, default => null, }; } if (!$expr instanceof Node\Expr\BinaryOp) { return null; } $left = $this->constantNumericValue($expr->left, $nativeSemantics); $right = $this->constantNumericValue($expr->right, $nativeSemantics); if ($left === null || $right === null) { return null; } return match (true) { $expr instanceof Node\Expr\BinaryOp\Plus => $left + $right, $expr instanceof Node\Expr\BinaryOp\Minus => $left - $right, $expr instanceof Node\Expr\BinaryOp\Mul => $left * $right, $expr instanceof Node\Expr\BinaryOp\Div => $this->constantDivisionValue( $left, $right, $nativeSemantics ), $expr instanceof Node\Expr\BinaryOp\Mod => is_int($left) && is_int($right) && $right !== 0 ? $left % $right : null, $expr instanceof Node\Expr\BinaryOp\BitwiseAnd => is_int($left) && is_int($right) ? $left & $right : null, $expr instanceof Node\Expr\BinaryOp\BitwiseOr => is_int($left) && is_int($right) ? $left | $right : null, $expr instanceof Node\Expr\BinaryOp\BitwiseXor => is_int($left) && is_int($right) ? $left ^ $right : null, $expr instanceof Node\Expr\BinaryOp\ShiftLeft => is_int($left) && is_int($right) ? $this->constantShiftValue($left, $right, true) : null, $expr instanceof Node\Expr\BinaryOp\ShiftRight => is_int($left) && is_int($right) ? $this->constantShiftValue($left, $right, false) : null, default => null, }; } protected function constantDivisionValue(int|float $left, int|float $right, bool $nativeSemantics): int|float|null { if ($right == 0) { return null; } if ($nativeSemantics && is_int($left) && is_int($right)) { if ($left === PHP_INT_MIN && $right === -1) { return null; } return intdiv($left, $right); } return $left / $right; } protected function constantShiftValue(int $value, int $shift, bool $left): ?int { if ($shift < 0) { return null; } if ($shift >= PHP_INT_SIZE * 8) { return $left ? 0 : ($value < 0 ? -1 : 0); } return $left ? $value << $shift : $value >> $shift; } protected function handleNestedConstantDivisionByZero( NodeAbstract $right, string $op, string $leftExpr, string $rightExpr ): ?string { if (($op !== '/' && $op !== '%') || $this->isZeroLiteral($right)) { return null; } $rightValue = $this->constantNumericValue($right, $this->nativeTypes); if ($rightValue === null || $rightValue != 0) { return null; } if ($this->nativeTypes) { $this->fatalError($right, 'Constant division or modulo by zero has undefined behavior in C++ native mode'); } // Preserve PHP's catchable DivisionByZeroError for a nested constant // zero. Literal zero keeps the compiler's established diagnostic. return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))'; } protected function genFloatLiteral(float $value): string { $text = sprintf('%.17g', $value); // Make sure the literal is parsed as a C++ double. if (!str_contains($text, '.') && !str_contains(strtolower($text), 'e')) { $text .= '.0'; } return $text; } protected function shouldMaterializeOrderedOperand(NodeAbstract $expr): bool { if ($expr instanceof Expr\BinaryOp) { return $this->shouldMaterializeOrderedOperand($expr->left) || $this->shouldMaterializeOrderedOperand($expr->right); } return $expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall || $expr instanceof Expr\New_ || $expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef || $expr instanceof Expr\AssignOp || $expr instanceof Expr\PostInc || $expr instanceof Expr\PostDec || $expr instanceof Expr\PreInc || $expr instanceof Expr\PreDec || $expr instanceof Expr\Print_ || $expr instanceof Expr\Array_ || $expr instanceof Expr\ArrayDimFetch || $expr instanceof Expr\PropertyFetch || $expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\Ternary || $expr instanceof Expr\Match_ || $expr instanceof Expr\NullsafeMethodCall || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\Clone_ || $expr instanceof Expr\Include_ || $expr instanceof Expr\Eval_; } protected function parseOrderedBinaryOperand(NodeAbstract $expr): string { return $this->parseOrderedOperand($expr, true); } protected function parseOrderedOperand(NodeAbstract $expr, bool $numeric): string { $this->assertExprCanBeUsedAsValue($expr, 'operand'); if (!$this->shouldMaterializeOrderedOperand($expr)) { return $numeric ? $this->parseNumericIdentifier($expr) : $this->parseIdentifier($expr); } [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $this->appendCapturedStmtLinesToContext($beforeStmts); $type = $this->getOrderedOperandTmpType($expr, (string) $value); $tmpVar = $this->addTmpVar($type); $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $this->appendCapturedStmtLinesToContext($afterStmts); if ($type === Type::VAR) { // The declaration is function-scoped, but PHP releases an owned // expression temporary after the statement that consumes it. // Keeping the value here would extend object lifetimes (notably // WeakReference targets) until the native function returns. $this->context->afterStmtLines[] = $tmpVar . '.unset();'; } return $tmpVar; } protected function getOrderedOperandTmpType(NodeAbstract $expr, string $value): string { if ($expr instanceof Expr\BinaryOp) { $type = $this->detectTypeOfExpr($expr); return in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) ? $type : Type::VAR; } if ( $expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall ) { $type = $this->detectTypeOfExpr($expr); return in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) ? $type : Type::VAR; } if ($expr instanceof Expr\PropertyFetch) { $nativePropertyVar = $this->getNativePropertyVar($expr); if ($nativePropertyVar !== null && $nativePropertyVar === $value) { $info = $this->getObjectPropInfoByVar($nativePropertyVar); if ($info !== null) { return $info['type']; } $def = $this->getNativePropertyDef($expr); if ($def && $this->isNativePropertyTypedValue($expr)) { return $def->type; } } return Type::VAR; } if ($expr instanceof Expr\StaticPropertyFetch) { $def = $this->getNativePropertyDef($expr); if ($def && $this->isNativePropertyTypedValue($expr)) { return $def->type; } return Type::VAR; } if ($expr instanceof Expr\ArrayDimFetch) { return Type::VAR; } $type = $this->detectTypeOfExpr($expr); return $type; } protected function appendCapturedStmtLinesToContext(array $stmts): void { foreach ($stmts as $stmt) { $this->context->beforeStmtLines[] = $stmt; } } protected function parseBinaryOpPlus(Expr\BinaryOp\Plus $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '+'); } protected function parseBinaryOpMul(Expr\BinaryOp\Mul $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '*'); } protected function parseBinaryOpConcat(Expr\BinaryOp\Concat $expr): string { return $this->parseFlattenedConcat($expr); } protected function parseFlattenedConcat(NodeAbstract $expr, array $prefixExpressions = []): string { $items = []; $this->flattenConcatExpr($expr, $items); $argList = $prefixExpressions; foreach ($items as $item) { // Keep one operand so concat still performs PHP string coercion. // Prefix expressions are operands too (for example, the left-hand // value of `.=`), so an empty RHS literal can be omitted there. if ($argList !== [] && $this->isScalarString($item) && $item->value === '') { continue; } $type = $this->detectTypeOfExpr($item); // C++17 evaluates the braced-list elements in order. The temporary // is still required because lowering a later operand may append // captured beforeStmtLines ahead of the entire concat expression; // without it, those statements could overtake an earlier Call. $parsed = $this->parseOrderedOperand($item, false); $argList[] = $this->prepareConcatOperand($parsed, $type); } return Symbol::concat() . '({' . implode(', ', $argList) . '})'; } protected function prepareConcatOperand(string $expr, string $type): string { if (in_array($type, [Type::STR, Type::INT, Type::FLOAT, Type::BOOL], true)) { return $expr; } // Keep conversions of objects/arrays/any values at their original // operand position. Moving them into concat() would evaluate all later // operands before __toString() or a conversion error is triggered. return $this->convertExprToStringByType($expr, $type); } protected function flattenConcatExpr(NodeAbstract $expr, array &$items): void { if ($expr instanceof Expr\BinaryOp\Concat) { $this->flattenConcatExpr($expr->left, $items); $this->flattenConcatExpr($expr->right, $items); } else { $items[] = $expr; } } protected function parseBinaryOpSmaller(Expr\BinaryOp\Smaller $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<')); } protected function parseBinaryOpShiftLeft(Expr\BinaryOp\ShiftLeft $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '<<'); } protected function parseBinaryOpShiftRight(Expr\BinaryOp\ShiftRight $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '>>'); } protected function parseBinaryOpMod(Expr\BinaryOp\Mod $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '%'); } protected function parseBinaryOpGreater(Expr\BinaryOp\Greater $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>')); } protected function parseBinaryOpPow(Expr\BinaryOp\Pow $expr): string { $pythonOperator = $this->parsePythonBinaryOperator($expr); if ($pythonOperator !== null) { return $pythonOperator; } $this->assertExprCanBeUsedAsValue($expr->left, 'binary operand'); $this->assertExprCanBeUsedAsValue($expr->right, 'binary operand'); $leftType = $this->detectTypeOfExpr($expr->left); $rightType = $this->detectTypeOfExpr($expr->right); if ($leftType === Type::DECIMAL || $rightType === Type::DECIMAL || $leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) { $this->fatalError($expr, "Operator '**' is not supported for Decimal or BigFloat; use pow() where supported"); } if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) { $leftExpr = $this->parseOrderedOperand($expr->left, false); $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== Type::BIGINT) { $leftExpr = $this->convertBigIntExpr($leftExpr, $leftType); } if ($rightType !== Type::BIGINT) { $rightExpr = $this->convertBigIntExpr($rightExpr, $rightType); } return 'php::BigInt::pow(' . $leftExpr . ', ' . $rightExpr . ')'; } $left = $this->parseOrderedOperand($expr->left, false); $right = $this->parseOrderedOperand($expr->right, false); return 'php::fn::pow(' . $left . ', ' . $right . ')'; } protected function parseBinaryOpBitwiseAnd(Expr\BinaryOp\BitwiseAnd $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '&'); } protected function parseBinaryOpBitwiseOr(Expr\BinaryOp\BitwiseOr $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '|'); } protected function parseBinaryOpBitwiseXor(Expr\BinaryOp\BitwiseXor $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '^'); } protected function parseCompareExpr(NodeAbstract $expr): string { $this->assertExprCanBeUsedAsValue($expr, 'comparison operand'); // PHPX 与 bool 值比较会出现重载错误,所以需要转换成 bool 值 if ($this->isScalarBool($expr)) { return $this->getBoolValue($expr); } return $this->parseOrderedOperand($expr, false); } protected function parseBinaryOpEqual(Expr\BinaryOp\Equal $expr): string { $pythonOperator = $this->parsePythonBinaryOperator($expr); if ($pythonOperator !== null) { return $pythonOperator; } return $this->genBigNumericCmp($expr, ' == 0') ?? 'php::equals(' . $this->parseCompareExpr($expr->left) . ', ' . $this->parseCompareExpr($expr->right) . ')'; } protected function parseBinaryOpNotEqual(Expr\BinaryOp\NotEqual $expr): string { $pythonOperator = $this->parsePythonBinaryOperator($expr); if ($pythonOperator !== null) { return $pythonOperator; } return $this->genBigNumericCmp($expr, ' != 0') ?? '!php::equals(' . $this->parseCompareExpr($expr->left) . ', ' . $this->parseCompareExpr($expr->right) . ')'; } protected function parseBinaryOpIdentical(Expr\BinaryOp $expr): string { $pythonOperator = $this->parsePythonBinaryOperator($expr); if ($pythonOperator !== null) { return $pythonOperator; } $left = $this->parseCompareExpr($expr->left); $right = $this->parseCompareExpr($expr->right); if ($right === 'nullptr') { // The left operand may itself be an assignment or another compound // expression. Parenthesize it before invoking Variant::isNull(), or // C++ binds the member access to the assignment's RHS instead. return '(' . $left . ').isNull()'; } if ($optimized = $this->optimizeIdenticalOp($expr->left, $expr->right, $left, $right)) { return $optimized; } return 'php::same(' . $left . ', ' . $right . ')'; } /** * Use compile-time type info to optimize === and !== . * When both sides are the same narrowed primitive type, emit direct C++ == . * When both are narrowed but different types, === is always false. */ private function optimizeIdenticalOp(NodeAbstract $astLeft, NodeAbstract $astRight, string $cppLeft, string $cppRight): ?string { $primitiveTypes = [Type::INT, Type::FLOAT, Type::BOOL]; $leftType = $this->detectTypeOfExpr($astLeft); $rightType = $this->detectTypeOfExpr($astRight); if ($leftType === null || $rightType === null) { return null; } if (!in_array($leftType, $primitiveTypes, true) || !in_array($rightType, $primitiveTypes, true)) { return null; } if ($leftType === $rightType) { if ($leftType === Type::BOOL) { $cppLeft = $this->nativeBoolLiteral($astLeft) ?? $cppLeft; $cppRight = $this->nativeBoolLiteral($astRight) ?? $cppRight; } return $cppLeft . ' == ' . $cppRight; } return 'false'; } /** * Strict comparisons between native booleans must use C++ bool literals. * parseCompareExpr() normally emits php::true_/php::false_ Variants because * dynamic comparisons need zvals, but those wrappers are incorrect once * optimizeIdenticalOp() selects a direct primitive comparison. */ private function nativeBoolLiteral(NodeAbstract $expr): ?string { if (!$this->isScalarBool($expr)) { return null; } return strcasecmp($expr->name->toString(), 'true') === 0 ? 'true' : 'false'; } protected function parseBinaryOpLogicalAnd(Expr\BinaryOp\LogicalAnd|Expr\BinaryOp\BooleanAnd $expr): string { return $this->parseShortCircuitLogicalOp($expr->left, $expr->right, '&&'); } protected function parseBinaryOpLogicalOr(Expr\BinaryOp\LogicalOr|Expr\BinaryOp\BooleanOr $expr): string { return $this->parseShortCircuitLogicalOp($expr->left, $expr->right, '||'); } protected function parseShortCircuitLogicalOp(NodeAbstract $left, NodeAbstract $right, string $op): string { $this->assertExprCanBeUsedAsCondition($left, 'logical operand'); $this->assertExprCanBeUsedAsCondition($right, 'logical operand'); $leftExpr = $this->parseNumericIdentifier($left); $this->checkVarMustExist($left, $leftExpr); $rightBeforeStmtCount = count($this->context->beforeStmtLines); $rightAfterStmtCount = count($this->context->afterStmtLines); $rightExpr = $this->parseNumericIdentifier($right); $rightBeforeStmts = array_slice($this->context->beforeStmtLines, $rightBeforeStmtCount); $rightAfterStmts = array_slice($this->context->afterStmtLines, $rightAfterStmtCount); $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $rightBeforeStmtCount); $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount); $this->checkVarMustExist($right, $rightExpr); $leftBool = $this->convertPythonObjectToBool($left, (string) $leftExpr) ?? $this->convertBoolExpr((string) $leftExpr, $this->detectTypeOfExpr($left)); $rightBool = $this->convertPythonObjectToBool($right, (string) $rightExpr) ?? $this->convertBoolExpr((string) $rightExpr, $this->detectTypeOfExpr($right)); if (!$rightBeforeStmts && !$rightAfterStmts) { return '(' . $leftBool . ' ' . $op . ' ' . $rightBool . ')'; } $shortCircuitValue = $op === '&&' ? 'false' : 'true'; $rightCondition = $op === '&&' ? $leftBool : '!(' . $leftBool . ')'; $code = '[&]() -> bool {'; $code .= $this->getIndent() . 'if (' . $rightCondition . ') {'; $code .= $this->formatCapturedStmtLines($rightBeforeStmts); if ($rightAfterStmts) { $rightTmpVar = $this->addTmpVar(Type::VAR); $code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';'; $code .= $this->formatCapturedStmtLines($rightAfterStmts); $rightExpr = $rightTmpVar; $rightBool = $this->convertPythonObjectToBool($right, $rightExpr) ?? $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right)); } $code .= $this->getIndent() . 'return ' . $rightBool . ';'; $code .= $this->getIndent() . '}'; $code .= $this->getIndent() . 'return ' . $shortCircuitValue . ';'; $code .= $this->getIndent() . '}()'; return $code; } protected function parseBinaryOpLogicalXor(Expr\BinaryOp\LogicalXor $expr): string { $this->assertExprCanBeUsedAsCondition($expr->left, 'logical operand'); $this->assertExprCanBeUsedAsCondition($expr->right, 'logical operand'); $left = $this->parseOrderedBinaryOperand($expr->left); $right = $this->parseOrderedBinaryOperand($expr->right); $leftBool = $this->convertPythonObjectToBool($expr->left, $left) ?? $this->convertBoolExpr($left, $this->detectTypeOfExpr($expr->left)); $rightBool = $this->convertPythonObjectToBool($expr->right, $right) ?? $this->convertBoolExpr($right, $this->detectTypeOfExpr($expr->right)); return '(' . $leftBool . ' != ' . $rightBool . ')'; } protected function parseBinaryOpSmallerOrEqual(Expr\BinaryOp\SmallerOrEqual $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '<=')); } protected function parseBinaryOpGreaterOrEqual(Expr\BinaryOp\GreaterOrEqual $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '>=')); } protected function parseBinaryOpSpaceship(Expr\BinaryOp\Spaceship $expr): string { return $this->genBigNumericCmp($expr) ?? 'php::compare(' . $this->parseOrderedOperand($expr->left, false) . ', ' . $this->parseOrderedOperand($expr->right, false) . ')'; } protected function genBigNumericCmp(Expr\BinaryOp $expr, string $suffix = ''): ?string { $leftType = $this->detectTypeOfExpr($expr->left); $rightType = $this->detectTypeOfExpr($expr->right); $bigTypes = [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT]; if (in_array($leftType, $bigTypes, true) && in_array($rightType, $bigTypes, true) && $leftType !== $rightType) { $this->fatalError( $expr, 'Cannot compare different Big* types implicitly; convert both operands to the same type explicitly' ); } if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) { $leftExpr = $this->parseOrderedOperand($expr->left, false); $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== Type::BIGFLOAT) { $leftExpr = $this->convertBigFloatExpr($leftExpr, $leftType); } if ($rightType !== Type::BIGFLOAT) { $rightExpr = $this->convertBigFloatExpr($rightExpr, $rightType); } return 'php::BigFloat::cmp(' . $leftExpr . ', ' . $rightExpr . ')' . $suffix; } if ($leftType === Type::BIGINT || $rightType === Type::BIGINT) { $leftExpr = $this->parseOrderedOperand($expr->left, false); $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== Type::BIGINT) { $leftExpr = $this->convertBigIntExpr($leftExpr, $leftType); } if ($rightType !== Type::BIGINT) { $rightExpr = $this->convertBigIntExpr($rightExpr, $rightType); } return 'php::BigInt::cmp(' . $leftExpr . ', ' . $rightExpr . ')' . $suffix; } if ($leftType === Type::DECIMAL || $rightType === Type::DECIMAL) { $leftExpr = $this->parseOrderedOperand($expr->left, false); $rightExpr = $this->parseOrderedOperand($expr->right, false); if ($leftType !== Type::DECIMAL) { $leftExpr = $this->convertDecimalExpr($leftExpr, $leftType, $expr->left); } if ($rightType !== Type::DECIMAL) { $rightExpr = $this->convertDecimalExpr($rightExpr, $rightType, $expr->right); } return 'php::Decimal::cmp(' . $leftExpr . ', ' . $rightExpr . ')' . $suffix; } return null; } protected function parseBinaryOpCoalesce(Expr\BinaryOp\Coalesce $expr): string { return $this->parseValueSelection($expr, $expr->left, $expr->right, self::OP_ISSET); } protected function parseBinaryOpNotIdentical(Expr\BinaryOp $expr): string { return $this->parsePythonBinaryOperator($expr) ?? '!(' . $this->parseBinaryOpIdentical($expr) . ')'; } protected function parseBinaryOpDiv(Expr\BinaryOp\Div $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '/'); } protected function guardLiteralDivisionByZero(NodeAbstract $right, string $op): void { if (($op === '/' or $op === '%' or $op === '/=' or $op === '%=') and $this->isZeroLiteral($right)) { $this->fatalError($right, 'Cannot divide or modulo by zero'); } } protected function parseBinaryOpMinus(Expr\BinaryOp\Minus $expr): string { return $this->parsePythonBinaryOperator($expr) ?? $this->parseBinaryOp($expr->left, $expr->right, '-'); } }