From 4b8d0eb6854362db5dc53f687d7b49da9d01b6b1 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 21 Aug 2026 11:36:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=9F=E6=88=90=E7=9A=84?= =?UTF-8?q?=20C++=20=E4=BB=A3=E7=A0=81=E4=BD=93=E7=A7=AF=EF=BC=8C=E5=87=8F?= =?UTF-8?q?=E5=B0=91=E6=97=A0=E6=95=88=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpunit/code/generated-code-comments.php | 7 + phpunit/code/generated-code-use-spacing.php | 14 ++ .../code/native-property-write-conversion.php | 12 +- phpunit/src/ClassTest.php | 2 +- phpunit/src/EmptyTranslationUnitTest.php | 149 ++++++++++++++++++ phpunit/src/GeneratedCodeCommentTest.php | 66 ++++++++ phpunit/src/NativePropertyTest.php | 4 + phpunit/src/TypeCheckGeneratorTest.php | 40 ++++- src/Build/SourcePipelineTrait.php | 12 +- src/CompilerBase.php | 11 +- src/Generator/TypeCheckGenerator.php | 53 +++---- src/Parser/AssignOpTrait.php | 4 +- src/Parser/ConditionalControlTrait.php | 2 +- src/Parser/FunctionCallTrait.php | 4 +- src/Parser/MethodCallTrait.php | 30 ++-- src/Parser/NullsafeAccessTrait.php | 6 +- src/Parser/PropertyAccessTrait.php | 6 +- src/Parser/SelectionExpressionTrait.php | 7 +- src/Resolver/DeclarationSymbolTrait.php | 5 +- src/Translator.php | 37 ++++- 20 files changed, 398 insertions(+), 73 deletions(-) create mode 100644 phpunit/code/generated-code-comments.php create mode 100644 phpunit/code/generated-code-use-spacing.php create mode 100644 phpunit/src/EmptyTranslationUnitTest.php create mode 100644 phpunit/src/GeneratedCodeCommentTest.php diff --git a/phpunit/code/generated-code-comments.php b/phpunit/code/generated-code-comments.php new file mode 100644 index 00000000..73eeb7c8 --- /dev/null +++ b/phpunit/code/generated-code-comments.php @@ -0,0 +1,7 @@ +dynamicMethod(); + return $object; +} diff --git a/phpunit/code/generated-code-use-spacing.php b/phpunit/code/generated-code-use-spacing.php new file mode 100644 index 00000000..88d83639 --- /dev/null +++ b/phpunit/code/generated-code-use-spacing.php @@ -0,0 +1,14 @@ +value = $nativeValue; $box->value = $dynamicValue; + $box->name = $dynamicName; + $box->items = $dynamicItems; } diff --git a/phpunit/src/ClassTest.php b/phpunit/src/ClassTest.php index 7369a355..d67f65df 100644 --- a/phpunit/src/ClassTest.php +++ b/phpunit/src/ClassTest.php @@ -557,7 +557,7 @@ class ClassTest extends \BaseTest $cppFile = $compiler->convertFile($testFile); $this->assertStringContainsString( - '// Stmt_Expression(Expr_StaticCall)', + 'this_.call(php_get_persistent_method(', file_get_contents($cppFile), ); } diff --git a/phpunit/src/EmptyTranslationUnitTest.php b/phpunit/src/EmptyTranslationUnitTest.php new file mode 100644 index 00000000..8327be59 --- /dev/null +++ b/phpunit/src/EmptyTranslationUnitTest.php @@ -0,0 +1,149 @@ +projectDir = sys_get_temp_dir() . '/typephp_empty_translation_' . bin2hex(random_bytes(6)); + mkdir($this->projectDir, 0777, true); + + global $translator; + $this->compiler = CompilerTest::create($this->projectDir); + $translator = $this->compiler; + } + + protected function tearDown(): void + { + $this->removeDirectory($this->projectDir); + parent::tearDown(); + } + + public function testTraitOnlySourceIsNotAddedToCompilation(): void + { + $trait = $this->writeSource('CompileTimeOnlyTrait.php', <<<'PHP' +writeSource('program.php', <<<'PHP' +compiler->addFiles($files); + foreach ($files as $file) { + $this->compiler->prepareFile($file); + } + + $traitCpp = $this->compiler->getCppFile($trait); + $traitObject = $this->compiler->getObjectFile($traitCpp); + if (!is_dir(dirname($traitCpp))) { + mkdir(dirname($traitCpp), 0777, true); + } + file_put_contents($traitCpp, 'stale translation unit'); + file_put_contents($traitObject, 'stale object'); + file_put_contents($traitObject . '.typephp-cache', 'stale metadata'); + + $sources = $this->compiler->convert($files); + + self::assertNotContains($traitCpp, $sources); + self::assertContains($this->compiler->getCppFile($program), $sources); + self::assertFileDoesNotExist($traitCpp); + self::assertFileDoesNotExist($traitObject); + self::assertFileDoesNotExist($traitObject . '.typephp-cache'); + self::assertFileExists($this->compiler->getArgInfoHeaderFile($trait)); + } + + public function testFileContainingTraitAndFunctionStillEmitsTranslationUnit(): void + { + $source = $this->writeSource('mixed.php', <<<'PHP' +compiler->addFiles([$source]); + $this->compiler->prepareFile($source); + $cppFile = $this->compiler->convertFile($source); + + self::assertNotNull($cppFile); + self::assertFileExists($cppFile); + self::assertStringContainsString('return php::toInt(7L);', file_get_contents($cppFile)); + } + + public function testCompileTimeOnlyLibraryStillEmitsExtensionSource(): void + { + $source = $this->writeSource('LibraryTrait.php', <<<'PHP' +compiler->setBuildMode('lib'); + $this->compiler->setOutputPath($this->projectDir . '/app'); + $this->compiler->addFiles([$source]); + $this->compiler->prepareFile($source); + $sources = $this->compiler->convert([$source]); + + self::assertCount(1, $sources); + self::assertStringContainsString('extension-app.cc', $sources[0]); + self::assertFileExists($sources[0]); + self::assertFileDoesNotExist($this->compiler->getCppFile($source)); + } + + private function writeSource(string $name, string $source): string + { + $file = $this->projectDir . '/' . $name; + file_put_contents($file, $source); + return $file; + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + foreach (array_diff(scandir($directory), ['.', '..']) as $entry) { + $path = $directory . '/' . $entry; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($directory); + } +} diff --git a/phpunit/src/GeneratedCodeCommentTest.php b/phpunit/src/GeneratedCodeCommentTest.php new file mode 100644 index 00000000..5b2694e6 --- /dev/null +++ b/phpunit/src/GeneratedCodeCommentTest.php @@ -0,0 +1,66 @@ +setValue($compiler, true); + } + + $source = ROOT_PATH . '/phpunit/code/generated-code-comments.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + $code = file_get_contents($generated); + self::assertIsString($code); + return $code; + } + + private function compileFile(string $file): string + { + global $translator; + + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $source = ROOT_PATH . '/phpunit/code/' . $file; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $generated = $compiler->convertFile($source); + self::assertNotNull($generated); + $code = file_get_contents($generated); + self::assertIsString($code); + return $code; + } + + public function testReleaseCodeOmitsCompilerExplanatoryComments(): void + { + $code = $this->compileProbe(false); + + self::assertStringNotContainsString('// Stmt_', $code); + self::assertStringNotContainsString('// Nullsafe Operator:', $code); + self::assertStringNotContainsString('// Method Call:', $code); + } + + public function testDebugCodeKeepsCompilerExplanatoryComments(): void + { + $code = $this->compileProbe(true); + + self::assertStringContainsString('// Stmt_', $code); + self::assertStringContainsString('// Nullsafe Operator:', $code); + } + + public function testCompileTimeUseDeclarationsDoNotEmitBlankLines(): void + { + $code = $this->compileFile('generated-code-use-spacing.php'); + + self::assertDoesNotMatchRegularExpression('/0,};(?:[ \t]*\R){3,}/', $code); + } +} diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index ffb2ba9a..fedcccb2 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -100,7 +100,11 @@ class NativePropertyTest extends \BaseTest $code = file_get_contents($outputFile); $this->assertStringContainsString(' = nativeValue;', $code); $this->assertStringContainsString(' = php::toIntExact(dynamicValue, "NativePropertyWriteConversionBox::$value");', $code); + $this->assertStringContainsString(' = php::toStringExact(dynamicName, "NativePropertyWriteConversionBox::$name");', $code); + $this->assertStringContainsString(' = php::toArrayExact(dynamicItems, "NativePropertyWriteConversionBox::$items");', $code); $this->assertStringNotContainsString(' = php::toInt(nativeValue);', $code); + $this->assertStringNotContainsString('php::toString(([&]() -> php::Var', $code); + $this->assertStringNotContainsString('php::toArray(([&]() -> php::Var', $code); } public function testNativeThisPropertyWriteUsesExactHelperOnNativeReference(): void diff --git a/phpunit/src/TypeCheckGeneratorTest.php b/phpunit/src/TypeCheckGeneratorTest.php index 7c5d4bb4..3cf4c85e 100644 --- a/phpunit/src/TypeCheckGeneratorTest.php +++ b/phpunit/src/TypeCheckGeneratorTest.php @@ -4,6 +4,7 @@ use TypePhp\Entity\ArgInfo; use TypePhp\CompilerTest; use TypePhp\Entity\ClassDef; use TypePhp\Entity\FunctionDef; +use TypePhp\Type; class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase { @@ -30,6 +31,7 @@ class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase $argInfo->name = 'value'; $argInfo->typeStr = 'int|string'; + $this->setProtectedProperty($compiler, 'noLiteralStrings', true); $functionDef->returnTypeCheck = [['kind' => 'isInt'], ['kind' => 'isString']]; $functionDef->returnTypeStr = 'int|string'; @@ -41,7 +43,10 @@ class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase $returnCode = $this->invokeMethod($compiler, 'genUnionReturnCheck', ['retval']); $this->assertSame('Foo\\Bar\\Demo::run', $callableName); - $this->assertStringContainsString('Foo\\\\Bar\\\\Demo::run(): Argument #', $paramExpr); + $this->assertStringContainsString('php::throwArgumentTypeError(', $paramExpr); + $this->assertStringContainsString('Foo\\\\Bar\\\\Demo::run', $paramExpr); + $this->assertStringNotContainsString('must be of type', $paramExpr); + $this->assertStringContainsString('php::throwReturnTypeError(', $returnCode); $this->assertStringContainsString('Foo\\\\Bar\\\\Demo::run', $returnCode); } @@ -53,6 +58,7 @@ class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase $argInfo->name = 'value'; $argInfo->typeStr = 'int|string'; + $this->setProtectedProperty($compiler, 'noLiteralStrings', true); $functionDef->returnTypeCheck = [['kind' => 'isInt'], ['kind' => 'isString']]; $functionDef->returnTypeStr = 'int|string'; @@ -64,7 +70,37 @@ class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase $returnCode = $this->invokeMethod($compiler, 'genUnionReturnCheck', ['retval']); $this->assertSame('Foo\\Bar\\run', $callableName); - $this->assertStringContainsString('Foo\\\\Bar\\\\run(): Argument #', $paramExpr); + $this->assertStringContainsString('php::throwArgumentTypeError(', $paramExpr); + $this->assertStringContainsString('Foo\\\\Bar\\\\run', $paramExpr); + $this->assertStringNotContainsString('must be of type', $paramExpr); + $this->assertStringContainsString('php::throwReturnTypeError(', $returnCode); $this->assertStringContainsString('Foo\\\\Bar\\\\run', $returnCode); } + + public function testStrictScalarChecksKeepTheFastCheckAndDelegateColdErrors(): void + { + $compiler = CompilerTest::create(ROOT_PATH); + $functionDef = new FunctionDef('run', 'php::Int', 'Foo\\Bar'); + $argInfo = new ArgInfo(); + $argInfo->name = 'value'; + $argInfo->phpName = 'value'; + $argInfo->type = Type::INT; + + $this->setProtectedProperty($compiler, 'classDef', null); + $this->setProtectedProperty($compiler, 'functionDef', $functionDef); + + $paramCode = $this->invokeMethod( + $compiler, + 'genStrictScalarParamCheck', + [$argInfo, 'raw_value', 'Foo\\Bar\\run', '1'], + ); + $returnCode = $this->invokeMethod($compiler, 'genStrictScalarReturnCheck', ['retval', Type::INT]); + + $this->assertStringContainsString('raw_value.isInt()', $paramCode); + $this->assertStringContainsString('php::throwArgumentTypeError(', $paramCode); + $this->assertStringNotContainsString('must be of type', $paramCode); + $this->assertStringContainsString('retval.isInt()', $returnCode); + $this->assertStringContainsString('php::throwReturnTypeError(', $returnCode); + $this->assertStringNotContainsString('must be of type', $returnCode); + } } diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index d8268f8f..1ba70006 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -250,6 +250,7 @@ trait SourcePipelineTrait public function convert(array $files): array { $sourceFiles = []; + $validSourceCount = 0; // 生成 C++ 文件 foreach ($files as $k => $file) { try { @@ -260,7 +261,10 @@ trait SourcePipelineTrait } else { continue; } - $sourceFiles[] = $cppFile; + $validSourceCount++; + if ($cppFile !== null) { + $sourceFiles[] = $cppFile; + } } catch (Unsupported $e) { echo ' unsupported syntax: ' . $e->getMessage() . "\n"; echo ' skip: ' . $file . "\n"; @@ -268,7 +272,11 @@ trait SourcePipelineTrait } } - if (empty($sourceFiles)) { + // A valid PHP input may intentionally emit no standalone translation + // unit (for example a compile-time trait or an interface). The shared + // extension source still carries its runtime metadata, so only reject + // an input set in which no supported source was converted at all. + if ($validSourceCount === 0) { $this->stop('No valid source file found'); } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c0aacaf4..f1324856 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1590,7 +1590,7 @@ class CompilerBase implements PropertyAccessContext $class = 'Stmt_Expression(' . $v->expr->getType() . ')'; } - return $this->getIndent() . '// ' . $class . ' [' . $v->getStartLine() . ':' . $v->getEndLine() . ']'; + return '// ' . $class . ' [' . $v->getStartLine() . ':' . $v->getEndLine() . ']'; } /** @@ -1698,8 +1698,10 @@ class CompilerBase implements PropertyAccessContext $this->context->afterStmtLines = []; $result = ''; $this->writeLog('Line ' . $this->getLine($v) . ': ' . $class); - $lines[] = $this->genDebugInfo($v); - $lines[] = $this->getComment($v, $class); + if ($this->debug) { + $lines[] = $this->genDebugInfo($v); + $lines[] = $this->getComment($v, $class); + } switch ($class) { case 'Stmt_Expression': $v->expr->setAttribute(self::ATTR_STATEMENT_EXPRESSION, true); @@ -3681,6 +3683,9 @@ class CompilerBase implements PropertyAccessContext protected function formatCppLineComment(string $label, string $text): string { + if (!$this->debug) { + return ''; + } $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $text)); $padding = str_repeat(' ', strlen($label)); $comments = []; diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index fc8ea15b..11910d41 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -58,15 +58,10 @@ trait TypeCheckGenerator } $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); - $format = $this->genCharPtr($callableName . '(): Argument #', true) - . ' ZEND_LONG_FMT ' - . $this->genCharPtr( - ' ($' . $paramName . ') must be of type ' . $this->strictScalarTypeName($argInfo->type) - . ', %s given', - true - ); - $throwExpr = 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' - . $argNoExpr . ', ' . $valueExpr . '.typeStr())'; + $throwExpr = 'php::throwArgumentTypeError(' . $valueExpr . ', ' + . $this->getLiteralString($callableName) . ', ' . $argNoExpr . ', ' + . $this->getLiteralString($paramName) . ', ' + . $this->getLiteralString($this->strictScalarTypeName($argInfo->type)) . ')'; $code = $this->getIndent() . 'if (UNEXPECTED(!(' . $this->genStrictScalarCondition($valueExpr, $argInfo->type) . '))) {' . PHP_EOL; @@ -84,17 +79,13 @@ trait TypeCheckGenerator } $fnName = $this->getTypeCheckCallableName(); - $format = $this->genCharPtr( - $fnName . '(): Return value must be of type ' . $this->strictScalarTypeName($returnType) - . ', %s returned', - true - ); - $code = $this->getIndent() . 'if (UNEXPECTED(!(' . $this->genStrictScalarCondition($valueExpr, $returnType) . '))) {' . PHP_EOL; $this->indentLevel++; - $code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, ' - . $format . ', ' . $valueExpr . '.typeStr());' . PHP_EOL; + $code .= $this->getIndent() . 'php::throwReturnTypeError(' . $valueExpr . ', ' + . $this->getLiteralString($fnName) . ', ' + . $this->getLiteralString($this->strictScalarTypeName($returnType)) . ', ' + . $this->escapeBool(true) . ');' . PHP_EOL; $this->indentLevel--; $code .= $this->getIndent() . '}' . PHP_EOL; return $code; @@ -392,11 +383,9 @@ trait TypeCheckGenerator { $fnName = $this->getTypeCheckCallableName(); $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); - $format = $this->genCharPtr($fnName . '(): Argument #', true) - . ' ZEND_LONG_FMT ' - . $this->genCharPtr(' ($' . $paramName . ') must be of type ' . $argInfo->typeStr . ', %s given', true); - return 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' - . $argNoExpr . ', ' . $valueExpr . '.typeStr())'; + return 'php::throwArgumentTypeError(' . $valueExpr . ', ' + . $this->getLiteralString($fnName) . ', ' . $argNoExpr . ', ' + . $this->getLiteralString($paramName) . ', ' . $this->getLiteralString($argInfo->typeStr) . ')'; } protected function genUnionReturnCheck(string $varName): string @@ -421,12 +410,12 @@ trait TypeCheckGenerator $fnName = $this->getTypeCheckCallableName(); $typeStr = $this->functionDef->returnTypeStr; - $format = $this->genCharPtr($fnName . '(): Return value must be of type ' . $typeStr . ', %s given', true); - $code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck); $code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL; $this->indentLevel++; - $code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' . $varName . '.typeStr());' . PHP_EOL; + $code .= $this->getIndent() . 'php::throwReturnTypeError(' . $varName . ', ' + . $this->getLiteralString($fnName) . ', ' . $this->getLiteralString($typeStr) . ', ' + . $this->escapeBool(false) . ');' . PHP_EOL; $this->indentLevel--; $code .= $this->getIndent() . '}' . PHP_EOL; @@ -515,11 +504,9 @@ trait TypeCheckGenerator protected function genClosureParamTypeErrorExpr(ArgInfo $argInfo, string $valueExpr, string $argNoExpr): string { $paramName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); - $format = $this->genCharPtr('{closure}(): Argument #', true) - . ' ZEND_LONG_FMT ' - . $this->genCharPtr(' ($' . $paramName . ') must be of type ' . $argInfo->typeStr . ', %s given', true); - return 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' - . $argNoExpr . ', ' . $valueExpr . '.typeStr())'; + return 'php::throwArgumentTypeError(' . $valueExpr . ', ' + . $this->getLiteralString('{closure}') . ', ' . $argNoExpr . ', ' + . $this->getLiteralString($paramName) . ', ' . $this->getLiteralString($argInfo->typeStr) . ')'; } protected function genClosureReturnCheck(string $varName): string @@ -542,12 +529,12 @@ trait TypeCheckGenerator $orExpr = implode(' || ', $conditions); $typeStr = $this->context->closureReturnTypeStr; - $format = $this->genCharPtr('{closure}(): Return value must be of type ' . $typeStr . ', %s given', true); - $code = $this->genCompositeIntToFloatCoercion($varName, $typeCheck); $code .= $this->getIndent() . 'if (UNEXPECTED(!(' . $orExpr . '))) {' . PHP_EOL; $this->indentLevel++; - $code .= $this->getIndent() . 'php::throwExceptionEx(zend_ce_type_error, 0, ' . $format . ', ' . $varName . '.typeStr());' . PHP_EOL; + $code .= $this->getIndent() . 'php::throwReturnTypeError(' . $varName . ', ' + . $this->getLiteralString('{closure}') . ', ' . $this->getLiteralString($typeStr) . ', ' + . $this->escapeBool(false) . ');' . PHP_EOL; $code .= $this->getIndent() . 'return php::null;' . PHP_EOL; $this->indentLevel--; $code .= $this->getIndent() . '}' . PHP_EOL; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 79f0273b..4ad41096 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -678,7 +678,7 @@ trait AssignOpTrait $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); if ($propertyWriteTarget !== null && ($propertyDef = $this->getNativePropertyDef($left)) !== null) { - $effectiveRightType = $rightExprType === Type::VAR && $this->getNativeScalarPropertyTypeCheckHelper($propertyDef) !== null + $effectiveRightType = $rightExprType === Type::VAR && $this->getFixedPropertyTypeCheckHelper($propertyDef) !== null ? $propertyDef->type : $rightExprType; return $var . ' = ' . $this->convertNativePropertyWriteExpr($propertyDef->type, $effectiveRightType, $rightExpr); @@ -1007,7 +1007,7 @@ trait AssignOpTrait if ($rightType === Type::VAR) { $rightExpr = $this->wrapObjectPropertyAssignTypeCheck($node->var, $node->expr, $rightExpr); } - $effectiveRightType = $rightType === Type::VAR && $this->getNativeScalarPropertyTypeCheckHelper($def) !== null + $effectiveRightType = $rightType === Type::VAR && $this->getFixedPropertyTypeCheckHelper($def) !== null ? $def->type : $rightType; diff --git a/src/Parser/ConditionalControlTrait.php b/src/Parser/ConditionalControlTrait.php index 9bf66f48..1b16b760 100644 --- a/src/Parser/ConditionalControlTrait.php +++ b/src/Parser/ConditionalControlTrait.php @@ -18,7 +18,7 @@ trait ConditionalControlTrait $arms[] = [$elseif->cond, $elseif->stmts]; } - return $this->parseBeforeStmtLines() . PHP_EOL . $this->parseIfChain($arms, $v->else, 0) . PHP_EOL; + return $this->parseBeforeStmtLines() . PHP_EOL . $this->getIndent() . $this->parseIfChain($arms, $v->else, 0) . PHP_EOL; } protected function parseIfChain(array $arms, ?Node\Stmt\Else_ $else, int $index): string diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 601ead67..5a59ca57 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -190,7 +190,9 @@ trait FunctionCallTrait } $placeHolder = $this->identifierToStr($expr->name); $fn = $this->getFuncPtr($name); - $this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()'); + if ($this->debug) { + $this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()'); + } } else { $tmpVar = $this->addTmpVar(Type::VAR); $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($expr->name) . ';'; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 17564a7b..42e65e97 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -580,10 +580,12 @@ trait MethodCallTrait } $this->fatalError($expr, "Cannot call method `{$methodName}()` on variable of type {$type}"); } - $this->context->beforeStmtLines[] = $this->formatCppLineComment( - 'Method Call: ', - $object . '->' . $this->parseIdentifier($expr->name) . '()' - ); + if ($this->debug) { + $this->context->beforeStmtLines[] = $this->formatCppLineComment( + 'Method Call: ', + $object . '->' . $this->parseIdentifier($expr->name) . '()' + ); + } $nativeFunc = false; try { $nativeFunc = $this->findNativeMethod($expr, $object, $this->parseIdentifier($expr->name)); @@ -808,10 +810,12 @@ trait MethodCallTrait $method = $this->parseIdentifier($expr->name); $methodPtr = $this->identifierToStr($expr->name, literal: true); $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; - $this->context->beforeStmtLines[] = $this->formatCppLineComment( - 'Static Method Call: ', - 'static::' . $method . '()' - ); + if ($this->debug) { + $this->context->beforeStmtLines[] = $this->formatCppLineComment( + 'Static Method Call: ', + 'static::' . $method . '()' + ); + } $placeHolder = $this->genArray([Symbol::getCalledClass(), $methodPtr]); // 用于在按引用参数检测时解析方法签名(late static binding 在当前类层级中解析) $rtFunc = $method; @@ -829,10 +833,12 @@ trait MethodCallTrait $method = $this->parseIdentifier($expr->name); $rtFunc = $method; $rtClass = $class; - $this->context->beforeStmtLines[] = $this->formatCppLineComment( - 'Static Method Call: ', - $class . '::' . $method . '()' - ); + if ($this->debug) { + $this->context->beforeStmtLines[] = $this->formatCppLineComment( + 'Static Method Call: ', + $class . '::' . $method . '()' + ); + } if ($this->isNameExpr($expr->class) and $this->isIdExpr($expr->name)) { $callScope = [$this->genCharPtr($class, true), $this->genCharPtr($method)]; diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index cff6e088..d992448e 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -45,7 +45,9 @@ trait NullsafeAccessTrait $list = []; $ownedTmpVars = []; - $comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])); + $comment = $this->debug + ? $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])) . PHP_EOL + : ''; while (1) { if ($expr instanceof Expr\NullsafePropertyFetch) { @@ -83,7 +85,7 @@ trait NullsafeAccessTrait $last = array_key_last($list); $tmpFn = $this->genTmpVarName(); - $code = $comment . PHP_EOL . 'auto ' . $tmpFn . ' = [&]() -> ' . Type::VAR . '{' . PHP_EOL; + $code = $comment . 'auto ' . $tmpFn . ' = [&]() -> ' . Type::VAR . '{' . PHP_EOL; foreach ($list as $key => $item) { $tmpVar = $this->addTmpVar($key !== $last ? Type::OBJECT : Type::VAR); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index e7162a72..f9455623 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -661,7 +661,7 @@ trait PropertyAccessTrait if ($rightType !== Type::VAR && $this->canAssignStaticTypeToObjectProperty($def, $rightType)) { return $rightExpr; } - if ($rightType === Type::VAR && ($helper = $this->getNativeScalarPropertyTypeCheckHelper($def)) !== null) { + if ($rightType === Type::VAR && ($helper = $this->getFixedPropertyTypeCheckHelper($def)) !== null) { return $helper . '(' . $rightExpr . ', ' . $this->genCharPtr($this->getObjectPropertyTypeCheckDisplayName($left), true) . ')'; } @@ -745,7 +745,7 @@ trait PropertyAccessTrait ], true); } - protected function getNativeScalarPropertyTypeCheckHelper(PropertyDef $def): ?string + protected function getFixedPropertyTypeCheckHelper(PropertyDef $def): ?string { if (!empty($def->typeCheck) || $def->class !== '' || $def->nullable) { return null; @@ -755,6 +755,8 @@ trait PropertyAccessTrait Type::INT => 'php::toIntExact', Type::FLOAT => 'php::toFloatExact', Type::BOOL => 'php::toBoolExact', + Type::STR => 'php::toStringExact', + Type::ARRAY => 'php::toArrayExact', default => null, }; } diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index d8841f2f..0c32005c 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -286,8 +286,11 @@ trait SelectionExpressionTrait $this->checkVarMustExist($right, $rightExpr); $tmpVar = $this->addTmpVar(Type::VAR); + $comment = $this->debug + ? $this->formatCppLineComment('Expr: ', $this->printer->prettyPrintExpr($expr)) . PHP_EOL + : ''; if ($rightBeforeStmts || $rightAfterStmts) { - $code = $this->formatCppLineComment('Expr: ', $this->printer->prettyPrintExpr($expr)) . PHP_EOL . + $code = $comment . 'if (' . $condExpr . ') {' . PHP_EOL . $this->getIndent() . $tmpVar . ' = ' . $leftExpr . ';' . PHP_EOL . '} else {' . PHP_EOL; @@ -305,7 +308,7 @@ trait SelectionExpressionTrait $code .= '}'; $this->context->beforeStmtLines[] = $code; } else { - $this->context->beforeStmtLines[] = $this->formatCppLineComment('Expr: ', $this->printer->prettyPrintExpr($expr)) . PHP_EOL . + $this->context->beforeStmtLines[] = $comment . $tmpVar . ' = ' . $condExpr . ' ? ' . $leftExpr . ' : ' . $rightExpr . ';'; } $expr->setAttribute('replace', $tmpVar); diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index c9a70c22..cc2949bb 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -66,10 +66,8 @@ trait DeclarationSymbolTrait return Type::VAR; } - - protected function parseUse(Node\Stmt\Use_ $v2): string + protected function parseUse(Node\Stmt\Use_ $v2): void { - $code = ''; foreach ($v2->uses as $use) { $id = $this->parseIdentifier($use->name); $type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type; @@ -107,7 +105,6 @@ trait DeclarationSymbolTrait } } } - return $code; } protected function parseGroupUse(Node\Stmt\GroupUse $node): void diff --git a/src/Translator.php b/src/Translator.php index 6fe05ff5..296f12f6 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -532,7 +532,7 @@ class Translator extends Preprocessor $this->formatCppCode($file); } - public function convertFile(string $file): string + public function convertFile(string $file): ?string { $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); try { @@ -543,11 +543,15 @@ class Translator extends Preprocessor try { $cppCode = $this->doConvert($phpCode); $cppFile = $this->getCppFile($file); - $this->save($cppCode, $cppFile); + if ($cppCode === '') { + $this->removeEmptyTranslationUnitArtifacts($cppFile); + } else { + $this->save($cppCode, $cppFile); + } $this->phpSrcFiles[] = $file; // 生成 stub 文件,依赖 convert 阶段的 use 等信息 $this->genStubFile($this->file); - return $cppFile; + return $cppCode === '' ? null : $cppFile; } catch (Redo $e) { continue; } @@ -557,6 +561,20 @@ class Translator extends Preprocessor } } + /** + * Remove artifacts left by an earlier build when a PHP source no longer + * emits a C++ translation unit. Trait-only files are the common case. + */ + private function removeEmptyTranslationUnitArtifacts(string $cppFile): void + { + $objectFile = $this->getObjectFile($cppFile); + foreach ([$cppFile, $objectFile, $this->getMiscObjectCacheMetadataFile($objectFile)] as $artifact) { + if (is_file($artifact) && !unlink($artifact)) { + throw new \RuntimeException("Unable to remove stale generated artifact: {$artifact}"); + } + } + } + public function getRegisterClassFunctionArgs(ClassDef|InterfaceDef $classDef): string { return implode(', ', $this->getRegisterClassFunctionCeList($classDef)); @@ -2557,7 +2575,7 @@ CODE; $cppCode .= $this->parseClass($v); break; case 'Stmt_Use': - $cppCode .= $this->parseUse($v) . PHP_EOL; + $this->parseUse($v); break; case 'Stmt_GroupUse': $this->parseGroupUse($v); @@ -2601,6 +2619,15 @@ CODE; foreach ($this->constData as $name => $data) { $constDataCode .= 'static const unsigned char ' . $name . '[] = {' . $data . '};' . PHP_EOL; } + + // Preparing and converting a compile-time-only source is still + // required for diagnostics, symbol collection and trait AST + // composition. Avoid creating a header-only .cc file when that work + // produced no C++ entity. + if (trim($constDataCode . $cppCode) === '') { + return ''; + } + $constDataCode .= PHP_EOL; return $this->genIncludeHeaderFiles() . $constDataCode . $cppCode; @@ -2741,7 +2768,7 @@ CODE; $code .= $this->parseFunction($v2) . PHP_EOL; break; case 'Stmt_Use': - $code .= $this->parseUse($v2) . PHP_EOL; + $this->parseUse($v2); break; case 'Stmt_GroupUse': $this->parseGroupUse($v2);