From be5cc71a8b6b7bfe4d244b056a3ceec7e8755986 Mon Sep 17 00:00:00 2001 From: yuan-dian <33216505+yuan-dian@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:59:35 +0800 Subject: [PATCH] optimize: convert for-loop post-inc/dec to prefix to avoid zval copy (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * optimize: convert for-loop post-inc/dec to prefix to avoid zval copy For-loop post-expressions (third clause of `for (init; cond; post)`) always discard the return value, so $i++ is semantically identical to ++$i in this context. Converting to prefix increment/decrement avoids the zval copy + destructor overhead from operator++(int)'s return value, saving ~6ns per increment per loop iteration. Changes: - LoopControlTrait: convert PostInc→PreInc, PostDec→PreDec in for-loop post-expressions only (while/do-while body untouched) - Add phpunit test verifying generated C++ contains ++i/--i for for-loops and retains i++ in while/do-while bodies - Add test data file with 7 test functions covering basic, multi-post, nested, mixed, and while/do-while cases * fix: restrict for-loop post-expr rewrite to simple variables only --- phpunit/code/loop/for-loop-post-expr-opt.php | 97 ++++++++++++++++++++ phpunit/src/LoopControlTest.php | 31 +++++++ src/Parser/LoopControlTrait.php | 8 ++ 3 files changed, 136 insertions(+) create mode 100644 phpunit/code/loop/for-loop-post-expr-opt.php diff --git a/phpunit/code/loop/for-loop-post-expr-opt.php b/phpunit/code/loop/for-loop-post-expr-opt.php new file mode 100644 index 00000000..1679311b --- /dev/null +++ b/phpunit/code/loop/for-loop-post-expr-opt.php @@ -0,0 +1,97 @@ + 0; $i--) { + $sum += $i; + } +} + +function test_multi_post(): void { + for ($i = 0, $j = 10; $i < 5; $i++, $j--) { + } +} + +function test_mixed_post(): void { + $j = 0; + for ($i = 0; $i < 10; $i++, $j += 2) { + } +} + +function test_nested_for(): void { + for ($i = 0; $i < 5; $i++) { + for ($j = 0; $j < 5; $j++) { + } + } +} + +function test_while_not_affected(): void { + $i = 0; + while ($i < 10) { + $i++; + } +} + +function test_do_while_not_affected(): void { + $i = 0; + do { + $i++; + } while ($i < 10); +} + +// --- Cases that must NOT be rewritten (property/static-property/array-element) --- + +class Counter { + public int $value = 0; +} + +class StaticCounter { + public static int $count = 0; +} + +function test_property_post_inc_not_rewritten(): void { + $obj = new Counter(); + for ($i = 0; $i < 10; $i++) { + $obj->value++; + } +} + +function test_property_post_dec_not_rewritten(): void { + $obj = new Counter(); + for ($i = 0; $i < 10; $i++) { + $obj->value--; + } +} + +function test_static_property_post_inc_not_rewritten(): void { + for ($i = 0; $i < 10; $i++) { + StaticCounter::$count++; + } +} + +function test_static_property_post_dec_not_rewritten(): void { + for ($i = 0; $i < 10; $i++) { + StaticCounter::$count--; + } +} + +function test_array_element_post_inc_not_rewritten(): void { + $arr = [0, 0, 0]; + for ($i = 0; $i < 10; $i++) { + $arr[$i % 3]++; + } +} + +function test_array_element_post_dec_not_rewritten(): void { + $arr = [10, 10, 10]; + for ($i = 0; $i < 10; $i++) { + $arr[$i % 3]--; + } +} diff --git a/phpunit/src/LoopControlTest.php b/phpunit/src/LoopControlTest.php index d21a5b76..e80a0073 100644 --- a/phpunit/src/LoopControlTest.php +++ b/phpunit/src/LoopControlTest.php @@ -30,4 +30,35 @@ class LoopControlTest extends \BaseTest 'control-flow/while-body-defined-condition.php', ); } + + public function testForLoopPostIncConvertedToPreInc(): void + { + global $translator; + $compiler = \TypePhp\CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $testFile = __DIR__ . '/../code/loop/for-loop-post-expr-opt.php'; + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $cppFile = $compiler->convertFile($testFile); + $cpp = file_get_contents($cppFile); + + // for-loop post-expression $i++ → ++$i, $i-- → --$i + $this->assertStringContainsString('++i', $cpp); + $this->assertStringContainsString('--i', $cpp); + + // while/do-while body $i++ should NOT be converted — still i++ + $this->assertStringContainsString("\ti++;\n", $cpp); + + // property postfix must NOT be rewritten — still postfix + $this->assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*\+\+/', $cpp); + $this->assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*--/', $cpp); + + // static-property postfix must NOT be rewritten + $this->assertMatchesRegularExpression('/getStaticProperty\([^)]+\)[^;]*\+\+/', $cpp); + $this->assertMatchesRegularExpression('/getStaticProperty\([^)]+\)[^;]*--/', $cpp); + + // array-element postfix must NOT be rewritten + $this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*\+\+/', $cpp); + $this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*--/', $cpp); + } } diff --git a/src/Parser/LoopControlTrait.php b/src/Parser/LoopControlTrait.php index 501cd2d0..0012fa4d 100644 --- a/src/Parser/LoopControlTrait.php +++ b/src/Parser/LoopControlTrait.php @@ -85,6 +85,14 @@ trait LoopControlTrait $list_loop = []; foreach ($loop as $expr) { + // for-loop post-expressions discard return value, so $i++ ≡ ++$i. + // Only rewrite simple variables; TypePHP lowers prefix and postfix + // differently for compound lvalues (e.g. static-property write-back). + if ($expr instanceof Node\Expr\PostInc && $this->isVarExpr($expr->var)) { + $expr = new Node\Expr\PreInc($expr->var, $expr->getAttributes()); + } elseif ($expr instanceof Node\Expr\PostDec && $this->isVarExpr($expr->var)) { + $expr = new Node\Expr\PreDec($expr->var, $expr->getAttributes()); + } [$loopExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $loopExpr = $this->stringifyParsedExpr($loopExpr); if ($beforeStmts || $afterStmts) {