feat(parser): optimize string concatenation assignment operations

- Preserve PHP's concat-assignment operation for statically typed strings using in-place append
- Add parseInPlaceStringConcatAssign method to handle string concatenation assignments efficiently
- Prevent O(n^2) work when rebuilding target = concat(target, rhs) operations
- Keep compound RHS evaluated completely before target changes in concat assignments
- Optimize native scalar binary operand handling to avoid unnecessary Variant boxing
- Add native scalar call results remain unboxed when ordered in native-types mode
- Create string concatenation assignment tests with proper PHP value and COW semantics
- Update version number from 1109 to 1110
master
韩天峰 5 days ago
parent 65d3710a61
commit 7418b6247e
  1. 4
      examples/bench.php
  2. 8
      phpunit/code/dynamic-scalar-binary-operands.php
  3. 10
      phpunit/code/native-scalar-binary-operands.php
  4. 10
      phpunit/code/string-concat-assign.php
  5. 40
      phpunit/src/NativeScalarBinaryOperandTest.php
  6. 24
      phpunit/src/StringConcatAssignTest.php
  7. 22
      src/Parser/AssignOpTrait.php
  8. 27
      src/Parser/BinaryOpTrait.php
  9. 40
      tests/compiler/operator/string-concat-assign-in-place.phpt
  10. 2
      version.txt

@ -179,11 +179,11 @@ function ary3(int $n) {
/****/ /****/
function fibo_r(int $n){ function fibo_r(int $n): int {
return(($n < 2) ? 1 : fibo_r($n - 2) + fibo_r($n - 1)); return(($n < 2) ? 1 : fibo_r($n - 2) + fibo_r($n - 1));
} }
function fibo(int $n) { function fibo(int $n): void {
$r = fibo_r($n); $r = fibo_r($n);
print "$r\n"; print "$r\n";
} }

@ -0,0 +1,8 @@
<?php
function recursivePhpInt(int $value): int
{
return $value < 2
? 1
: recursivePhpInt($value - 2) + recursivePhpInt($value - 1);
}

@ -0,0 +1,10 @@
<?php
use native_types;
function recursiveNativeInt(int $value): int
{
return $value < 2
? 1
: recursiveNativeInt($value - 2) + recursiveNativeInt($value - 1);
}

@ -0,0 +1,10 @@
<?php
function stringConcatAssignCodegen(string $suffix): string
{
$value = '';
$value .= 'hello';
$value .= ', ' . $suffix;
$result = ($value .= $value);
return $result;
}

@ -0,0 +1,40 @@
<?php
use TypePhp\CompilerTest;
final class NativeScalarBinaryOperandTest extends \BaseTest
{
public function testNativeScalarCallResultsRemainUnboxedWhenOrdered(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/native-scalar-binary-operands.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertStringContainsString('php::Int php_recursivenativeint(php::Int value)', $code);
self::assertStringNotContainsString('php::Var tmp_var_', $code);
}
public function testPhpCompatibleScalarCallResultsRemainBoxedWhenOrdered(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/dynamic-scalar-binary-operands.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertStringContainsString('php::Int php_recursivephpint(php::Int value)', $code);
self::assertStringContainsString('php::Var tmp_var_', $code);
}
}

@ -0,0 +1,24 @@
<?php
use TypePhp\CompilerTest;
final class StringConcatAssignTest extends \BaseTest
{
public function testTypedStringConcatAssignmentUsesInPlaceAppend(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/string-concat-assign.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertGreaterThanOrEqual(3, substr_count($code, 'value.append('));
self::assertStringContainsString('value.append(php::concat({', $code);
self::assertStringNotContainsString('value = php::concat({value,', $code);
}
}

@ -873,6 +873,9 @@ trait AssignOpTrait
if ($this->isArrayVar($node->var)) { if ($this->isArrayVar($node->var)) {
$this->fatalError($node->var, 'Cannot concat string to array'); $this->fatalError($node->var, 'Cannot concat string to array');
} }
if ($type === Type::STR) {
return $this->parseInPlaceStringConcatAssign($node, $var);
}
return $var . ' = ' . $this->parseFlattenedConcat($node->expr, [ return $var . ' = ' . $this->parseFlattenedConcat($node->expr, [
$this->prepareConcatOperand($var, $type), $this->prepareConcatOperand($var, $type),
]); ]);
@ -958,6 +961,25 @@ trait AssignOpTrait
return $var . ' ' . $op . ' (' . $expr . ')'; return $var . ' ' . $op . ' (' . $expr . ')';
} }
/**
* Preserve PHP's concat-assignment operation for statically typed strings.
* String::append() calls concat_function() with the target as both the
* result and left operand, allowing Zend to extend an unshared string in
* place. Rebuilding `target = concat(target, rhs)` would copy the complete
* prefix on every iteration and turn repeated `.=` into O(n^2) work.
*
* A compound RHS is still evaluated completely before the target changes.
* The comma expression keeps `.=` usable as a value expression.
*/
private function parseInPlaceStringConcatAssign(Expr\AssignOp\Concat $node, string $var): string
{
$right = $node->expr instanceof Expr\BinaryOp\Concat
? $this->parseFlattenedConcat($node->expr)
: $this->parseExprAsValue($node->expr);
return '(' . $var . '.append(' . $right . '), ' . $var . ')';
}
protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op): ?string protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op): ?string
{ {
if (!$this->isPropertyFetch($node->var)) { if (!$this->isPropertyFetch($node->var)) {

@ -542,6 +542,12 @@ trait BinaryOpTrait
$type = $this->getOrderedOperandTmpType($expr, (string) $value); $type = $this->getOrderedOperandTmpType($expr, (string) $value);
$tmpVar = $this->addTmpVar($type); $tmpVar = $this->addTmpVar($type);
} }
if ($this->nativeTypes && $this->isNativeType($type)) {
// A native temporary has a fixed C++ scalar ABI. The expression
// can still contain a dynamic operand (for example, an array
// element), so normalize it at the materialization boundary.
$value = $this->convertExprFromType($type, (string) $value);
}
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';';
$this->appendCapturedStmtLinesToContext($afterStmts); $this->appendCapturedStmtLinesToContext($afterStmts);
if ($this->isNativeObjectClass($nativeClass)) { if ($this->isNativeObjectClass($nativeClass)) {
@ -560,18 +566,25 @@ trait BinaryOpTrait
protected function getOrderedOperandTmpType(NodeAbstract $expr, string $value): string 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 ( if (
$expr instanceof Expr\FuncCall $expr instanceof Expr\BinaryOp
|| $expr instanceof Expr\FuncCall
|| $expr instanceof Expr\MethodCall || $expr instanceof Expr\MethodCall
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\StaticCall
) { ) {
$type = $this->detectTypeOfExpr($expr); $type = $this->detectTypeOfExpr($expr);
return in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true) ? $type : Type::VAR; if (
in_array($type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true)
|| ($this->nativeTypes && $this->isNativeType($type))
) {
// Calls and nested binary operands are materialized to preserve
// PHP's left-to-right evaluation order. In native-types mode
// their scalar result has a fixed C++ representation, so
// boxing it in a Variant would add dynamic arithmetic and zval
// lifetime work to otherwise native expressions.
return $type;
}
return Type::VAR;
} }
if ($expr instanceof Expr\PropertyFetch) { if ($expr instanceof Expr\PropertyFetch) {

@ -0,0 +1,40 @@
--TEST--
String concat assignment preserves PHP value, COW and expression semantics
--FILE--
<?php
declare(strict_types=1);
final class ConcatAssignStringable
{
public function __toString(): string
{
echo "convert\n";
return 'object';
}
}
function main(): void
{
$value = 'start';
$copy = $value;
$suffix = 'tail';
$value .= ':';
$value .= $suffix . ':' . new ConcatAssignStringable();
var_dump($value, $copy);
$self = 'ab';
$self .= $self;
var_dump($self);
$result = ($value .= '!');
var_dump($value, $result);
}
?>
--EXPECT--
convert
string(17) "start:tail:object"
string(5) "start"
string(4) "abab"
string(18) "start:tail:object!"
string(18) "start:tail:object!"

@ -1 +1 @@
1109 1110
Loading…
Cancel
Save