diff --git a/phpunit/code/hot-path-codegen.php b/phpunit/code/hot-path-codegen.php index 29574979..67b9bcbc 100644 --- a/phpunit/code/hot-path-codegen.php +++ b/phpunit/code/hot-path-codegen.php @@ -32,3 +32,15 @@ function hotPathCodegen(int $limit): int return $assigned + $length + strlen($safe) + strlen($ordered) + $value; } + +function hotPathTypedReads(array $hash, string $str, bool $flag, int $fallback): void +{ + $hashValue = $hash['value']; + $stringValue = $str[0]; + $arraySelection = $hash ?: null; + $boolSelection = $flag ?: $fallback; + + // A compound left operand still needs the generic chain helper so the + // array lookup is evaluated exactly once. + $chainedSelection = $hash['value'] ?: $fallback; +} diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index b6d1a86d..68688ad3 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -970,6 +970,54 @@ YAML); } } + public function testGeneratedRuntimeSymbolsUseProjectNamespace(): void + { + global $translator; + $testFile = ROOT_PATH . '/phpunit/code/compiler_api/extension_clean_maps.php'; + + foreach ([ + CompilerBase::BUILD_MODE_EXT => 'isolated_ext', + CompilerBase::BUILD_MODE_BIN => 'isolated_bin', + CompilerBase::BUILD_MODE_LIB => 'isolated_lib', + ] as $mode => $target) { + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $compiler->setBuildMode($mode); + $compiler->setTargetName($target); + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + + $dataFile = $this->testDir . '/' . $target . '_data_decl.h'; + $compiler->genDataDeclarations($dataFile); + $data = file_get_contents($dataFile); + $extension = file_get_contents($compiler->genExtension()); + $namespace = 'typephp_' . $target; + + $this->assertStringContainsString('namespace ' . $namespace . ' {', $data, $mode); + $this->assertStringContainsString('using namespace ' . $namespace . ';', $data, $mode); + $this->assertStringContainsString('zend_class_entry *php_get_class(', $data, $mode); + $this->assertStringContainsString('namespace ' . $namespace . ' {', $extension, $mode); + $this->assertStringContainsString('zend_class_entry *php_get_class(', $extension, $mode); + + if ($mode === CompilerBase::BUILD_MODE_BIN) { + $this->assertStringContainsString('zend_module_entry *php_embed_get_module()', $extension); + $this->assertStringContainsString( + 'return &' . $namespace . '::' . $namespace . '_module_entry;', + $extension, + ); + } elseif ($mode === CompilerBase::BUILD_MODE_LIB) { + $this->assertStringNotContainsString('zend_module_entry *php_embed_get_module()', $extension); + $this->assertStringContainsString( + 'zend_module_entry *php_' . $target . '_embed_get_module()', + $extension, + ); + } else { + $this->assertStringNotContainsString('php_embed_get_module', $extension, $mode); + } + } + } + public function testFunctionPointerCacheRejectsClassMethodNames(): void { $this->expectException(\LogicException::class); diff --git a/phpunit/src/HotPathCodegenTest.php b/phpunit/src/HotPathCodegenTest.php index d3df9603..e1130de3 100644 --- a/phpunit/src/HotPathCodegenTest.php +++ b/phpunit/src/HotPathCodegenTest.php @@ -11,8 +11,8 @@ final class HotPathCodegenTest extends \BaseTest self::assertStringContainsString('items.item(0L, true) = value;', $code); self::assertStringContainsString('items.append(value);', $code); self::assertStringContainsString('items.item(0L, true) += value;', $code); - self::assertStringContainsString('items.item(0L, true) += other.item(0L, false);', $code); - self::assertStringContainsString('items.item(2L, true) = other.item(0L, false);', $code); + self::assertStringContainsString('items.item(0L, true) += other.get(0L);', $code); + self::assertStringContainsString('items.item(2L, true) = other.get(0L);', $code); self::assertStringContainsString('items.offsetSet(0L,', $code); } @@ -36,6 +36,27 @@ final class HotPathCodegenTest extends \BaseTest self::assertDoesNotMatchRegularExpression('/php::Var (tmp_var_\d+);[\s\S]*?\\1 = limit--;/', $code); } + public function testTypedReadsAndSimpleShorthandTernariesUseFastPaths(): void + { + $code = $this->compileFixture(); + + self::assertMatchesRegularExpression( + '/hash\.get\(_literal_strings\[\d+\]\)/', + $code, + ); + self::assertStringContainsString('str.offsetGet(0L)', $code); + self::assertStringNotContainsString('php::notEmpty(hash, {},', $code); + self::assertStringNotContainsString('php::notEmpty(flag, {},', $code); + self::assertStringContainsString('php::toBool(hash)', $code); + self::assertStringContainsString('php::toBool(flag)', $code); + + // Compound receivers retain the evaluate-once chain implementation. + self::assertMatchesRegularExpression( + '/php::notEmpty\(hash, \{\{php::ArrayDimFetch, php::Var\(_literal_strings\[\d+\]\)\}\}, tmp_var_\d+\)/', + $code, + ); + } + private function compileFixture(): string { global $translator; diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index ca7b7c50..fb546174 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -203,9 +203,10 @@ trait ArrayExpressionTrait return $this->parseStdContainerDimFetch($node); } + $isGlobals = $this->isVarExpr($node->var) && $node->var->name === 'GLOBALS'; $var = $write ? $this->parseWritableIdentifier($node->var) : $this->parseIdentifier($node->var); if ($this->isVarExpr($node->var)) { - if ($var === 'GLOBALS') { + if ($isGlobals) { return $this->parseGlobalsArrayDimFetch($node); } if (!$this->hasVar($var)) { @@ -235,6 +236,32 @@ trait ArrayExpressionTrait } } else { $dim = $this->parseIdentifier($node->dim); + // Only fixed local variables and parameters are emitted as + // php::Array or php::Str. Globals/statics, properties, and call + // expressions may carry the same PHP type while their C++ storage + // remains php::Variant. + $fixedReceiver = $this->isVarExpr($node->var) + && !$this->hasScopeGlobalVar($var) + && !$this->hasStaticVar($var); + if (!$write && $fixedReceiver) { + $receiverType = $this->detectTypeOfExpr($node->var); + $dimType = $this->detectTypeOfExpr($node->dim); + if ($receiverType === Type::STR) { + $offset = $dimType === Type::INT ? $dim : 'php::toInt(' . $dim . ')'; + return $var . '.offsetGet(' . $offset . ')'; + } + if ($receiverType === Type::ARRAY) { + if ($dimType === Type::INT) { + return $var . '.get(' . $dim . ')'; + } + if ($dimType === Type::FLOAT) { + return $var . '.get(php::toInt(' . $dim . '))'; + } + if ($dimType === Type::STR) { + return $var . '.get(' . $dim . ')'; + } + } + } return $var . '.item(' . $dim . ', ' . $this->escapeBool($write) . ')'; } } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 6777c170..2878926b 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -911,6 +911,10 @@ trait AssignOpTrait return $this->parseStdContainerAssignOp($node, $op); } if ($this->canUpdateKnownArraySlotInPlace($node, $op)) { + if ($this->isVarExpr($node->var->var) && $node->var->var->name === 'GLOBALS') { + $slot = $this->parseGlobalsArrayDimFetch($node->var); + return $slot . ' ' . $op . ' ' . $this->parseExprAsValue($node->expr); + } $array = $this->parseWritableIdentifier($node->var->var); $dim = $this->parseIdentifier($node->var->dim); return $array . '.item(' . $dim . ', true) ' . $op . ' ' diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index 0e0e7fa6..2ffa32e0 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -284,15 +284,25 @@ trait SelectionExpressionTrait $this->checkVarMustExist($left, $leftExpr); } - $condExpr = $this->parseChainedExpr($left, $op, true); - $chainOpResult = $left->getAttribute('chainOpResult'); - if ($chainOpResult) { - $leftExpr = $chainOpResult; - } $leftType = $this->detectTypeOfExpr($left); - if ($op === self::OP_NOT_EMPTY - && in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) { - $condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')'; + $simpleShorthand = $op === self::OP_NOT_EMPTY + && $this->isVarExpr($left) + && $leftType !== Type::REF; + if ($simpleShorthand) { + // A local variable is already a stable value. Avoid routing it + // through the generic operation-chain walker, which otherwise + // copies the value into a Variant result before testing it. + $condExpr = $this->convertConditionExpr($left, $leftExpr); + } else { + $condExpr = $this->parseChainedExpr($left, $op, true); + $chainOpResult = $left->getAttribute('chainOpResult'); + if ($chainOpResult) { + $leftExpr = $chainOpResult; + } + if ($op === self::OP_NOT_EMPTY + && in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) { + $condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')'; + } } $rightBeforeStmtCount = count($this->context->beforeStmtLines); @@ -304,6 +314,19 @@ trait SelectionExpressionTrait $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount); $this->checkVarMustExist($right, $rightExpr); + if ($simpleShorthand && !$rightBeforeStmts && !$rightAfterStmts) { + // C++'s conditional operator evaluates the condition first and + // only the selected branch. Explicit Variant materialization is + // needed when the PHP branch types differ; otherwise C++ could + // choose a common scalar type (for example bool -> int). + $rightType = $this->detectTypeOfExpr($right); + if ($leftType !== $rightType) { + $leftExpr = 'php::Var(' . $leftExpr . ')'; + $rightExpr = 'php::Var(' . $rightExpr . ')'; + } + return '(' . $condExpr . ') ? (' . $leftExpr . ') : (' . $rightExpr . ')'; + } + $tmpVar = $this->addTmpVar(Type::VAR); $comment = $this->debug ? $this->formatCppLineComment('Expr: ', $this->printer->prettyPrintExpr($expr)) . PHP_EOL diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 70487131..055a7d26 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -266,11 +266,14 @@ trait TypeConversionTrait { $this->assertNativeObjectReferenceForbidden($expr, $expr); $this->checkLeftValue($expr); + if ($expr instanceof Node\Expr\ArrayDimFetch) { + return $this->parseArrayDimFetchUpdate($expr) . '.toReference()'; + } $var = $this->parseIdentifier($expr); if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) { $this->context->localVars[$var] = Type::VAR; } - return $this->parseIdentifier($expr) . '.toReference()'; + return $var . '.toReference()'; } } diff --git a/tests/compiler/optimizations/typed-read-fast-path.phpt b/tests/compiler/optimizations/typed-read-fast-path.phpt new file mode 100644 index 00000000..0789d3ec --- /dev/null +++ b/tests/compiler/optimizations/typed-read-fast-path.phpt @@ -0,0 +1,64 @@ +--TEST-- +typed array/string reads and shorthand ternaries preserve PHP semantics +--FILE-- + 1, + '12' => 'numeric key', + 'ref' => &$referenced, + ]; + + inspectTypedReads($values, '12', 'test', 1); + + $copy = $values['ref']; + $copy = 4; + var_dump($referenced, $copy); + + inspectSelections($values, '0', false); + inspectSelections([], 'ok', true); +} +?> +--EXPECT-- +int(1) +string(11) "numeric key" +string(1) "t" +string(1) "e" +string(1) "t" +int(3) +int(4) +array(3) { + ["value"]=> + int(1) + [12]=> + string(11) "numeric key" + ["ref"]=> + &int(3) +} +string(8) "fallback" +int(42) +array(1) { + [0]=> + string(8) "fallback" +} +string(2) "ok" +bool(true)