From 745556fee83a10b1742120a6df75144c3e915dde Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 21 Sep 2026 12:50:28 +0800 Subject: [PATCH] feat(compiler): track referenced classes and optimize runtime function calls - Add referencedClasses array to store fully resolved class names - Implement recordReferencedClass method to track class references - Enhance function call parsing with static function resolution - Add native function lookup and return type detection - Insert type conversion for runtime call results - Resolve extension dependencies based on internal functions and classes - Append extension requirements to generated module header - Update tests to validate extension dependency detection - Add test case for runtime builtin int property assignment - Modify polymorphic class dispatch to restore string representation --- phpunit/src/CompilerBaseApiTest.php | 46 ++++++++++++ phpunit/src/FuncCallOptimizerTest.php | 7 +- phpunit/src/PolymorphicClassDispatchTest.php | 7 +- src/CompilerBase.php | 23 ++++++ src/Parser/FunctionCallTrait.php | 26 +++++-- src/Parser/TypeConversionTrait.php | 17 +++++ src/Resolver/NameResolutionTrait.php | 10 +-- src/Translator.php | 73 ++++++++++++++++++- ...ntime-builtin-int-property-assignment.phpt | 49 +++++++++++++ 9 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 tests/compiler/object_property/runtime-builtin-int-property-assignment.phpt diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 1f38e2d0..d8da01a3 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -667,6 +667,52 @@ YAML); ); } + public function testInternalSymbolExtensionsAreDetectedAsModuleDependencies(): void + { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('mbstring extension is not available'); + } + + global $translator; + $translator = $this->compiler; + $this->compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +extension-dependencies: + - MBSTRING +YAML); + file_put_contents(dirname($projectFile) . '/main.php', <<<'PHP' +invokeMethod('parseProjectYaml', $projectFile); + $this->compiler->addFiles($files); + $generatedSources = []; + foreach ($files as $file) { + $this->compiler->prepareFile($file); + $generatedFile = $this->compiler->convertFile($file); + if ($generatedFile !== null) { + $generatedSources[] = file_get_contents($generatedFile); + } + } + + $extension = file_get_contents($this->compiler->genExtension()); + + $this->assertStringContainsString('php::toInt(php::call(', implode("\n", $generatedSources)); + $this->assertSame(1, substr_count($extension, 'ZEND_MOD_REQUIRED("MBSTRING")')); + $this->assertStringContainsString('ZEND_MOD_REQUIRED("Core")', $extension); + $this->assertStringContainsString('ZEND_MOD_REQUIRED("date")', $extension); + $this->assertStringContainsString('ZEND_MOD_REQUIRED("Reflection")', $extension); + $this->assertStringNotContainsString('ZEND_MOD_REQUIRED("mbstring")', $extension); + } + public function testParseProjectYamlSupportsCustomFilenameAndRelativeBuildDir(): void { $projectFile = $this->createProjectFile(<<<'YAML' diff --git a/phpunit/src/FuncCallOptimizerTest.php b/phpunit/src/FuncCallOptimizerTest.php index 556c5ad4..78073d3f 100644 --- a/phpunit/src/FuncCallOptimizerTest.php +++ b/phpunit/src/FuncCallOptimizerTest.php @@ -28,8 +28,11 @@ final class FuncCallOptimizerTest extends BaseTest self::assertStringContainsString('php_optimizertypedfloat()', $code); self::assertStringContainsString('php_optimizerdynamicbool()', $code); self::assertMatchesRegularExpression('/php::toBool\(php_optimizertypedbool\(\)\);\s*php::fn::in_array/', $code); - self::assertMatchesRegularExpression('/php::toInt\(php_optimizertypedint\(\)\);\s*php::call/', $code); - self::assertMatchesRegularExpression('/php_optimizerdynamicbool\(\);\s*php::call/', $code); + self::assertMatchesRegularExpression( + '/php::toInt\(php_optimizertypedint\(\)\);\s*php::toBool\(php::call/', + $code, + ); + self::assertMatchesRegularExpression('/php_optimizerdynamicbool\(\);\s*php::toBool\(php::call/', $code); self::assertStringContainsString('php::fn::hypot(php::toFloat(', $code); self::assertStringContainsString('php::VarList{php::null}', $code); self::assertMatchesRegularExpression('/php::fn::json_decode\([^;]+php::null\);/', $code); diff --git a/phpunit/src/PolymorphicClassDispatchTest.php b/phpunit/src/PolymorphicClassDispatchTest.php index b6f69894..4260866f 100644 --- a/phpunit/src/PolymorphicClassDispatchTest.php +++ b/phpunit/src/PolymorphicClassDispatchTest.php @@ -24,15 +24,16 @@ final class PolymorphicClassDispatchTest extends BaseTest ); // Polymorphic get_class() must NOT fold or blindly invoke unchecked C++ helper; - // it must use runtime php::call to enforce PHP argument semantics (TypeError on null) + // it must use runtime php::call to enforce PHP argument semantics (TypeError on null), + // then restore the fixed string representation declared by get_class(). $persistentFunctions = (new \ReflectionProperty($compiler, 'persistentFuncMap'))->getValue($compiler); $literalStrings = (new \ReflectionProperty($compiler, 'literalStrings'))->getValue($compiler); self::assertArrayHasKey('get_class', $persistentFunctions); self::assertArrayHasKey('get_class', $literalStrings); self::assertMatchesRegularExpression( sprintf( - '/php_getpolymorphicclass\(\) \{.*?tmp_var_\d+ = \(php::call\(get_persistent_func' - . '\(PersistentFuncId\{%d\}, get_str\(%d\)\), php::VarList\{animal\}\)\);/s', + '/php_getpolymorphicclass\(\) \{.*?tmp_var_\d+ = \(php::toString\(php::call\(get_persistent_func' + . '\(PersistentFuncId\{%d\}, get_str\(%d\)\), php::VarList\{animal\}\)\)\);/s', $persistentFunctions['get_class'], $literalStrings['get_class'], ), diff --git a/src/CompilerBase.php b/src/CompilerBase.php index b4694cb4..14ef9c87 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -678,6 +678,8 @@ class CompilerBase implements PropertyAccessContext /** Whole-program usage collected during the convert phase. */ protected CompilationStatistics $compilationStatistics; + /** @var array Fully resolved class names referenced by source code. */ + protected array $referencedClasses = []; public function __construct(string $rootPath) { @@ -965,6 +967,15 @@ class CompilerBase implements PropertyAccessContext return $this->compilationStatistics; } + protected function recordReferencedClass(string $class): string + { + $class = ltrim($class, '\\'); + if ($class !== '') { + $this->referencedClasses[$class] = true; + } + return $class; + } + /** Record value types that survived into an emitted translation unit. */ protected function recordEmittedTypes(string $code): void { @@ -3472,6 +3483,18 @@ class CompilerBase implements PropertyAccessContext } if ($this->isNameExpr($expr->name)) { $name = $this->parseIdentifier($expr->name); + $functionTarget = $this->resolveStaticFunctionCallTarget($expr->name); + $nativeFunction = $this->findNativeFunction($functionTarget['nativeLookup']); + if ($nativeFunction !== false) { + return $this->getFunction($nativeFunction)->returnType; + } + // An unqualified call in a namespace may resolve to a + // runtime-provided namespaced function before PHP falls + // back to the global builtin. Its return representation is + // therefore dynamic when no compiled function was found. + if ($functionTarget['namespacedFallback']) { + return Type::VAR; + } $globalName = ltrim($name, '\\'); // Math function optimization: propagate Big* return types if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 990dae6d..65298b68 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -17,6 +17,7 @@ use PhpParser\Node\Expr\Variable; use PhpParser\NodeAbstract; use TypePhp\Metadata\Constants; use TypePhp\Exception\PlaceHolder; +use TypePhp\Resolver\Reflection; trait FunctionCallTrait { @@ -191,6 +192,7 @@ trait FunctionCallTrait protected function parseFuncCall(Expr\FuncCall $expr): string { $runtimeCallScope = null; + $runtimeResultType = null; $this->validateImmutableCall($expr); $pythonCall = $this->parsePythonFunctionCall($expr); if ($pythonCall !== null) { @@ -379,6 +381,12 @@ trait FunctionCallTrait CompilationStatistics::RUNTIME_FUNCTIONS, strtolower($globalName), ); + if ($functionTarget['definitelyGlobal'] && $this->isInternalFunction($globalName)) { + $returnType = Reflection::getFunction($globalName)?->getReturnType(); + if ($returnType instanceof \ReflectionNamedType && !$returnType->allowsNull()) { + $runtimeResultType = $this->detectFuncCallReturnType($globalName); + } + } $placeHolder = $this->getLiteralString($functionTarget['target']); $fn = $this->getFuncPtr($name); if ($this->debug) { @@ -399,18 +407,24 @@ trait FunctionCallTrait return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ')'; } $scopeArg = $runtimeCallScope === null ? '' : $runtimeCallScope . ', '; - return 'php::call(' . $scopeArg . $fn . ')'; + return $this->convertRuntimeCallResult( + $runtimeResultType, + 'php::call(' . $scopeArg . $fn . ')', + ); } try { if ($name === '' && $runtimeCallScope === null) { return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ', ' . $this->parseCallArgs($expr->args) . ')'; } - return $this->genRuntimeFunctionCall( - $fn, - $expr->args, - $name, - scope: $runtimeCallScope ?? '', + return $this->convertRuntimeCallResult( + $runtimeResultType, + $this->genRuntimeFunctionCall( + $fn, + $expr->args, + $name, + scope: $runtimeCallScope ?? '', + ), ); } catch (PlaceHolder) { return $this->genPlaceHolder($placeHolder); diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 373c4082..6312298d 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -264,6 +264,23 @@ trait TypeConversionTrait return $expr; } + /** + * php::call() returns a Variant even when the resolved internal function + * has a fixed Zend return type. Restore the C++ representation promised by + * static type detection before the value reaches typed native storage. + */ + protected function convertRuntimeCallResult(?string $type, string $expr): string + { + if ($type === null) { + return $expr; + } + $storageType = $this->getNativeType(Type::getReferencedType($type)); + if ($storageType === Type::VAR) { + return $expr; + } + return $this->convertExprFromType($storageType, $expr); + } + protected function convertVarType($var, $expr): string { if ($this->hasVar($var)) { diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 485b99cb..ac009e6f 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -24,7 +24,7 @@ trait NameResolutionTrait $this->error('Class name can not be empty'); } if ($class[0] === '\\') { - return ltrim($class, '\\'); + return $this->recordReferencedClass($class); } $ns2 = explode('\\', trim($class, '\\')); @@ -36,7 +36,7 @@ trait NameResolutionTrait if (count($ns2) > 1) { $ns .= '\\' . implode('\\', array_slice($ns2, 1)); } - return ltrim($ns, '\\'); + return $this->recordReferencedClass($ns); } foreach ($this->useNamespaces as $useNamespace) { @@ -53,7 +53,7 @@ trait NameResolutionTrait if (count($ns2) > 1) { foreach ($this->useNamespaces as $useNamespace) { if (strcasecmp(trim($useNamespace, '\\'), $class) === 0) { - return $class; + return $this->recordReferencedClass($class); } } } @@ -62,10 +62,10 @@ trait NameResolutionTrait $currentNamespace = $this->namespace; } if (!empty($currentNamespace)) { - return trim($currentNamespace, '\\') . '\\' . $class; + return $this->recordReferencedClass(trim($currentNamespace, '\\') . '\\' . $class); } - return $class; + return $this->recordReferencedClass($class); } /** diff --git a/src/Translator.php b/src/Translator.php index af750a84..0e2e233c 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -11,6 +11,7 @@ namespace TypePhp; use Ajaxray\AnsiKit\AnsiTerminal; use Ajaxray\AnsiKit\Components\Progressbar; use MJS\TopSort\Implementations\StringSort; +use TypePhp\Analysis\CompilationStatistics; use TypePhp\Analysis\LocalClosureAnalyzer; use TypePhp\Analysis\NativeObjectStackPromotionAnalyzer; use TypePhp\Analysis\SsaBuilder; @@ -1774,12 +1775,13 @@ PHP_RSHUTDOWN_FUNCTION({$moduleName}) { } CODE; - if ($this->extensionDependencies === []) { + $extensionDependencies = $this->resolveExtensionDependencies(); + if ($extensionDependencies === []) { $moduleHeader = ' STANDARD_MODULE_HEADER,'; } else { $dependencyArray = $moduleName . '_module_deps'; $code .= PHP_EOL . 'static const zend_module_dep ' . $dependencyArray . '[] = {' . PHP_EOL; - foreach ($this->extensionDependencies as $dependency) { + foreach ($extensionDependencies as $dependency) { $code .= ' ZEND_MOD_REQUIRED(' . $this->genCharPtr($dependency, true) . ')' . PHP_EOL; } $code .= ' ZEND_MOD_END' . PHP_EOL . '};' . PHP_EOL; @@ -1824,6 +1826,73 @@ CODE; return $file; } + /** + * Require every PHP extension that owns an internal function or class + * referenced by compiled sources. The compiler host supplies the ownership + * metadata via Reflection; Zend validates the resulting dependency list + * when the generated TypePHP module starts on the target host. + * + * @return list + */ + private function resolveExtensionDependencies(): array + { + $dependencies = $this->extensionDependencies; + $seen = []; + foreach ($dependencies as $dependency) { + $seen[strtolower($dependency)] = true; + } + + foreach (array_keys($this->compilationStatistics->get(CompilationStatistics::FUNCTIONS)) as $function) { + $reflection = Reflection::getFunction(ltrim($function, '\\')); + if ($reflection === null || !$reflection->isInternal()) { + continue; + } + $this->appendExtensionDependency( + $dependencies, + $seen, + $reflection->getExtensionName(), + ); + } + + $classes = $this->referencedClasses; + foreach (array_keys($this->compilationStatistics->get(CompilationStatistics::CLASSES)) as $class) { + $classes[$class] = true; + } + ksort($classes, SORT_STRING); + foreach (array_keys($classes) as $class) { + $reflection = Reflection::getClass(ltrim($class, '\\')); + if ($reflection === null || !$reflection->isInternal()) { + continue; + } + $this->appendExtensionDependency( + $dependencies, + $seen, + $reflection->getExtensionName(), + ); + } + + return $dependencies; + } + + /** + * @param list $dependencies + * @param array $seen + */ + private function appendExtensionDependency(array &$dependencies, array &$seen, mixed $extension): void + { + if (!is_string($extension) + || $extension === '' + || Reflection::isTypePhpExtension($extension) + ) { + return; + } + $key = strtolower($extension); + if (!isset($seen[$key])) { + $dependencies[] = $extension; + $seen[$key] = true; + } + } + public function getModuleName(): string { return Constants::EXTENSION_PREFIX . $this->targetName; diff --git a/tests/compiler/object_property/runtime-builtin-int-property-assignment.phpt b/tests/compiler/object_property/runtime-builtin-int-property-assignment.phpt new file mode 100644 index 00000000..a9e29953 --- /dev/null +++ b/tests/compiler/object_property/runtime-builtin-int-property-assignment.phpt @@ -0,0 +1,49 @@ +--TEST-- +Runtime-dispatched builtin int results support typed property assignments +--FILE-- +value = connection_status(); + } + + public function add(): void + { + $this->value += connection_status(); + } + + public function addViaLocal(): void + { + $status = connection_status(); + $this->value += $status; + } +} + +} + +namespace { + +function main(): void +{ + $value = new RuntimeBuiltinNamespace\RuntimeBuiltinIntProperty(); + $value->assign(); + var_dump($value->value); + $value->add(); + var_dump($value->value); + $value->addViaLocal(); + var_dump($value->value); +} + +} +?> +--EXPECT-- +int(0) +int(0) +int(0)