From 10f16726e0d9a54b235119932459a534de3a35e2 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 1 Sep 2026 16:40:27 +0800 Subject: [PATCH] optimize non-native integer hot paths --- phpunit/src/SsaAnalysisTest.php | 15 +++++- src/Optimizer/LoopVarOptimizer.php | 51 +++++++++++++++++-- src/Parser/AssignOpTrait.php | 12 ++++- src/Parser/BinaryOpTrait.php | 5 +- src/Translator.php | 10 +++- .../operator/local-var-compound-overflow.phpt | 18 +++++++ .../loop-var-overflow-semantics.phpt | 18 +++++++ 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 tests/compiler/operator/local-var-compound-overflow.phpt create mode 100644 tests/compiler/optimizations/loop-var-overflow-semantics.phpt diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 51e8e7bf..8ce937dc 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -1199,7 +1199,7 @@ class SsaAnalysisTest extends TestCase $this->assertArrayNotHasKey('i', $locals); } - public function testLoopVarOptimizerRejectsUnsafeUseAfterLoop(): void + public function testLoopVarOptimizerAllowsCheckedArithmeticAfterLoop(): void { $locals = $this->optimizeLoopVarsForCode(' $n = 10; @@ -1209,6 +1209,19 @@ class SsaAnalysisTest extends TestCase $x = $i + PHP_INT_MAX; '); + $this->assertSame(Type::INT, $locals['i'] ?? null); + } + + public function testLoopVarOptimizerRejectsMutatingUseAfterLoop(): void + { + $locals = $this->optimizeLoopVarsForCode(' + $n = 10; + for ($i = 0; $i < $n; $i++) { + echo $i; + } + $i += PHP_INT_MAX; + '); + $this->assertArrayNotHasKey('i', $locals); } diff --git a/src/Optimizer/LoopVarOptimizer.php b/src/Optimizer/LoopVarOptimizer.php index b7a4bee8..601d4a39 100644 --- a/src/Optimizer/LoopVarOptimizer.php +++ b/src/Optimizer/LoopVarOptimizer.php @@ -30,14 +30,18 @@ trait LoopVarOptimizer 'mb_substr_count' => true, ]; - protected function optimizeLoopVars(SsaBuilder $ssa): void + /** + * @return array Escaped local name => narrowed C++ type. + */ + protected function optimizeLoopVars(SsaBuilder $ssa): array { $stmts = $ssa->getStmts(); if (!$stmts) { - return; + return []; } $candidates = []; + $optimized = []; $this->collectLoopVarCandidates($stmts, [], $candidates); foreach ($candidates as $varName => $candidate) { @@ -65,7 +69,10 @@ trait LoopVarOptimizer continue; } $this->context->localVars[$escapedName] = Type::INT; + $optimized[$escapedName] = Type::INT; } + + return $optimized; } /** @@ -363,7 +370,24 @@ trait LoopVarOptimizer } if ($expr instanceof Expr\Variable && is_string($expr->name)) { - return $safeVars[$expr->name] ?? null; + if (isset($safeVars[$expr->name])) { + return $safeVars[$expr->name]; + } + + // Typed parameters already use a native php::Int slot. They are + // safe as exclusive loop bounds: `for ($i = 0; $i < $n; $i++)` + // cannot increment past PHP_INT_MAX. Their sign and distance from + // the integer limits are unknown, so do not accept them for the + // non-negative post-decrement or inclusive-bound cases. + $argumentName = $this->escapeVarName($expr->name); + if (($this->context->arguments[$argumentName] ?? null) === Type::INT) { + return [ + 'nonNegative' => false, + 'inclusiveSafe' => false, + ]; + } + + return null; } if ($this->isLoopIntCall($expr)) { @@ -388,6 +412,17 @@ trait LoopVarOptimizer return false; } + // This pass runs before statement conversion has populated all local + // variable types. Avoid asking the general expression detector to + // resolve chained/dynamic receivers here: apart from being needlessly + // expensive for unrelated assignments, that can report an undefined + // receiver before its preceding assignment has been converted. Direct + // calls such as `$object->toInt()` remain eligible. + if (($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) + && (!$expr->var instanceof Expr\Variable || !is_string($expr->var->name))) { + return false; + } + return $this->detectTypeOfExpr($expr) === Type::INT; } @@ -652,7 +687,15 @@ trait LoopVarOptimizer } if ($expr instanceof Expr\BinaryOp) { - if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()])) { + if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()]) + || $expr instanceof Expr\BinaryOp\Plus + || $expr instanceof Expr\BinaryOp\Minus + || $expr instanceof Expr\BinaryOp\Mul) { + // In ordinary PHP mode BinaryOpTrait routes native Int + // addition/subtraction/multiplication through php::Var's + // checked operators, preserving overflow-to-float semantics. + // Narrowing the loop counter therefore does not make these + // read-only expression uses native C++ arithmetic. return $this->loopExprHasIntHazard($expr->left, $varName, $allowedNodes) || $this->loopExprHasIntHazard($expr->right, $varName, $allowedNodes); } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 213f1e9d..9e9fe3a9 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -917,7 +917,14 @@ trait AssignOpTrait return $this->parseBigAssignOp($node, $var, $type, $expr, $rightType, $op); } - $rightExprStr = $this->convertExprType($expr, $type, $rightType); + // A dynamic local must retain the RHS runtime type. Variant's + // compound operators already implement PHP coercion and checked + // integer overflow; eagerly converting an int-looking expression + // both adds an unnecessary unbox/rebox pair and can collapse an + // overflowed float back to int before the assignment. + $rightExprStr = $type === Type::VAR + ? $expr + : $this->convertExprType($expr, $type, $rightType); if ($this->isAssignOpConcat($op)) { if ($this->isArrayVar($node->var)) { $this->fatalError($node->var, 'Cannot concat string to array'); @@ -933,6 +940,9 @@ trait AssignOpTrait $powExpr = 'php::fn::pow(' . $var . ', ' . $rightExprStr . ')'; return $var . ' = ' . $this->convertVarType($var, $powExpr); } + if ($type === Type::VAR && $op === '+=') { + return $var . '.addAssign(' . $rightExprStr . ')'; + } return $var . ' ' . $op . ' ' . $rightExprStr; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 4431d48f..c8b5c35c 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -169,7 +169,10 @@ trait BinaryOpTrait && in_array($op, ['+', '-', '*'], true) && $this->evaluateConstantIntArithmetic($left, $right, $op) === null ) { - return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))'; + // Keep the potentially widening result boxed, but pass the native + // RHS directly so PHPX can use its inline checked-int overload + // without constructing and destroying another temporary zval. + return '((php::Var(' . $leftExpr . ')) ' . $op . ' (' . $rightExpr . '))'; } return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))'; diff --git a/src/Translator.php b/src/Translator.php index fbadc576..f9cf7073 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4435,11 +4435,14 @@ CODE; $ssaBuilder->build(); $this->context->ssaBuilder = $ssaBuilder; $this->analyzeStableObjects($ssaBuilder); + // Range-proven loop counters are safe to narrow even without + // `use native_types`: the optimizer rejects counters whose PHP + // integer semantics could widen to float or otherwise escape. + $optimizedLoopVars = $this->optimizeLoopVars($ssaBuilder); if ($this->nativeTypes) { // Narrow local variable types based on SSA analysis. $this->optimizeVarTypes($ssaBuilder); - // Narrow range-proven loop counters and native property accesses. - $this->optimizeLoopVars($ssaBuilder); + // Narrow native property accesses. $this->optimizeObjectProps($ssaBuilder); } $this->context->resetAnalysisTemporaries( @@ -4449,6 +4452,9 @@ CODE; $oriNativeObjects, $oriNonNullNativeObjects, ); + foreach ($optimizedLoopVars as $varName => $type) { + $this->context->localVars[$varName] = $type; + } } $stmts = ''; diff --git a/tests/compiler/operator/local-var-compound-overflow.phpt b/tests/compiler/operator/local-var-compound-overflow.phpt new file mode 100644 index 00000000..d76e2804 --- /dev/null +++ b/tests/compiler/operator/local-var-compound-overflow.phpt @@ -0,0 +1,18 @@ +--TEST-- +Dynamic local compound assignment preserves PHP integer overflow promotion +--FILE-- + +--EXPECT-- +bool(true) +bool(true) diff --git a/tests/compiler/optimizations/loop-var-overflow-semantics.phpt b/tests/compiler/optimizations/loop-var-overflow-semantics.phpt new file mode 100644 index 00000000..738c22c3 --- /dev/null +++ b/tests/compiler/optimizations/loop-var-overflow-semantics.phpt @@ -0,0 +1,18 @@ +--TEST-- +Loop var optimizer preserves PHP overflow-to-float semantics without native_types +--FILE-- + +--EXPECT-- +int(9223372036854775807) +bool(true)