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
master
韩天峰 3 weeks ago
parent 6c0c26458f
commit 745556fee8
  1. 46
      phpunit/src/CompilerBaseApiTest.php
  2. 7
      phpunit/src/FuncCallOptimizerTest.php
  3. 7
      phpunit/src/PolymorphicClassDispatchTest.php
  4. 23
      src/CompilerBase.php
  5. 26
      src/Parser/FunctionCallTrait.php
  6. 17
      src/Parser/TypeConversionTrait.php
  7. 10
      src/Resolver/NameResolutionTrait.php
  8. 73
      src/Translator.php
  9. 49
      tests/compiler/object_property/runtime-builtin-int-property-assignment.phpt

@ -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'
<?php
function main(): void
{
strlen('typephp');
mb_strlen('类型');
DateTimeInterface::ATOM;
new ReflectionClass(stdClass::class);
}
PHP);
$files = $this->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'

@ -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);

@ -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'],
),

@ -678,6 +678,8 @@ class CompilerBase implements PropertyAccessContext
/** Whole-program usage collected during the convert phase. */
protected CompilationStatistics $compilationStatistics;
/** @var array<string, true> 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)

@ -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);

@ -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)) {

@ -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);
}
/**

@ -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<string>
*/
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<string> $dependencies
* @param array<string, true> $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;

@ -0,0 +1,49 @@
--TEST--
Runtime-dispatched builtin int results support typed property assignments
--FILE--
<?php
namespace RuntimeBuiltinNamespace {
class RuntimeBuiltinIntProperty
{
public int $value = 0;
public function assign(): void
{
$this->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)
Loading…
Cancel
Save