diff --git a/examples/bench.php b/examples/bench.php index 2a1b60f1..ec3dd722 100644 --- a/examples/bench.php +++ b/examples/bench.php @@ -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)); } -function fibo(int $n) { +function fibo(int $n): void { $r = fibo_r($n); print "$r\n"; } diff --git a/phpunit/code/dynamic-scalar-binary-operands.php b/phpunit/code/dynamic-scalar-binary-operands.php new file mode 100644 index 00000000..0ab18432 --- /dev/null +++ b/phpunit/code/dynamic-scalar-binary-operands.php @@ -0,0 +1,8 @@ +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); + } +} diff --git a/phpunit/src/StringConcatAssignTest.php b/phpunit/src/StringConcatAssignTest.php new file mode 100644 index 00000000..b988d08a --- /dev/null +++ b/phpunit/src/StringConcatAssignTest.php @@ -0,0 +1,24 @@ +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); + } +} diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index da1b7905..f48f772d 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -873,6 +873,9 @@ trait AssignOpTrait if ($this->isArrayVar($node->var)) { $this->fatalError($node->var, 'Cannot concat string to array'); } + if ($type === Type::STR) { + return $this->parseInPlaceStringConcatAssign($node, $var); + } return $var . ' = ' . $this->parseFlattenedConcat($node->expr, [ $this->prepareConcatOperand($var, $type), ]); @@ -958,6 +961,25 @@ trait AssignOpTrait 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 { if (!$this->isPropertyFetch($node->var)) { diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index a1e24477..768e3e18 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -542,6 +542,12 @@ trait BinaryOpTrait $type = $this->getOrderedOperandTmpType($expr, (string) $value); $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->appendCapturedStmtLinesToContext($afterStmts); if ($this->isNativeObjectClass($nativeClass)) { @@ -560,18 +566,25 @@ trait BinaryOpTrait 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\BinaryOp + || $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 ( + 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) { diff --git a/tests/compiler/operator/string-concat-assign-in-place.phpt b/tests/compiler/operator/string-concat-assign-in-place.phpt new file mode 100644 index 00000000..2816bb0d --- /dev/null +++ b/tests/compiler/operator/string-concat-assign-in-place.phpt @@ -0,0 +1,40 @@ +--TEST-- +String concat assignment preserves PHP value, COW and expression semantics +--FILE-- + +--EXPECT-- +convert +string(17) "start:tail:object" +string(5) "start" +string(4) "abab" +string(18) "start:tail:object!" +string(18) "start:tail:object!" diff --git a/version.txt b/version.txt index 304a6a57..96e38be9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1109 \ No newline at end of file +1110 \ No newline at end of file