feat(compiler): add compilation statistics collection and nano extension selection

- Introduce CompilationStatistics class to track function, class, type, and dynamic capability usage
- Add statistics collection to CompilerBase for functions, classes, and types during conversion
- Implement NanoExtensionSelector to convert statistics into selective extension component needs
- Add fine-grained feature selection for standard extension components like arrays, strings, filesystem
- Create ComposerNativeComponent class for selective composition of native package parts
- Extend ComposerNativePackage with component metadata parsing and validation
- Update NanoSourceComposer to use statistics-driven selective composition
- Add test coverage for compilation statistics and extension selection scenarios
- Include example code and comprehensive test suite for new
master
韩天峰 4 weeks ago
parent 7726f49cd7
commit a1782233ca
  1. 7
      phpunit/code/compilation_statistics.php
  2. 202
      phpunit/src/CompilationStatisticsTest.php
  3. 73
      src/Analysis/CompilationStatistics.php
  4. 22
      src/Build/ComposerNativeComponent.php
  5. 80
      src/Build/ComposerNativePackage.php
  6. 20
      src/Build/NanoExtensionSelection.php
  7. 401
      src/Build/NanoExtensionSelector.php
  8. 117
      src/Build/NanoSourceComposer.php
  9. 1
      src/Build/NativeCommandOptionsTrait.php
  10. 14
      src/Build/NativeSourceProjectBuilder.php
  11. 11
      src/Build/SourcePipelineTrait.php
  12. 44
      src/CompilerBase.php
  13. 25
      src/Parser/FunctionCallTrait.php
  14. 12
      src/Parser/UniversalMethodCall.php
  15. 19
      src/Translator.php

@ -0,0 +1,7 @@
<?php
function compilationStatisticsExample(): string
{
$value = json_encode(['time' => time()]);
return $value === false ? '' : $value;
}

@ -0,0 +1,202 @@
<?php
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\Build\NanoExtensionSelector;
use TypePhp\Build\NanoSourceComposer;
use TypePhp\CompilerTest;
use TypePhp\Type;
final class CompilationStatisticsTest extends BaseTest
{
public function testConvertCollectsFunctionAndEmittedTypeUsage(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$file = dirname(__DIR__) . '/code/compilation_statistics.php';
$compiler->addFiles([$file]);
$compiler->prepareFile($file);
$compiler->convertFile($file);
$statistics = $compiler->getCompilationStatistics();
self::assertTrue($statistics->has(CompilationStatistics::FUNCTIONS, 'json_encode'));
self::assertTrue($statistics->has(CompilationStatistics::FUNCTIONS, 'time'));
self::assertTrue($statistics->has(CompilationStatistics::DIRECT_FUNCTIONS, 'json_encode'));
self::assertTrue($statistics->has(CompilationStatistics::DIRECT_FUNCTIONS, 'time'));
self::assertTrue($statistics->has(CompilationStatistics::TYPES, Type::STR));
$selection = (new NanoExtensionSelector())->select($statistics, ['date', 'json']);
self::assertSame(['date', 'json'], $selection->extensions);
self::assertSame([], $selection->features);
}
public function testStatisticsAreReusableByCategory(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'json_encode');
$statistics->record(CompilationStatistics::FUNCTIONS, 'json_encode');
$statistics->record('future-module-data', 'example');
$statistics->finish();
self::assertSame(['json_encode' => 2], $statistics->get(CompilationStatistics::FUNCTIONS));
self::assertSame(['example' => 1], $statistics->get('future-module-data'));
}
public function testNanoSelectorUsesFunctionsClassesTypesAndDependencies(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'preg_match');
$statistics->record(CompilationStatistics::RUNTIME_FUNCTIONS, 'preg_match');
$statistics->record(CompilationStatistics::CLASSES, 'ArrayObject');
$statistics->record(CompilationStatistics::TYPES, Type::BIGINT);
$statistics->finish();
$selection = (new NanoExtensionSelector())->select(
$statistics,
['standard', 'date', 'hash', 'json', 'pcre', 'random', 'reflection', 'spl', 'filter', 'bcmath'],
);
self::assertFalse($selection->completeFallback);
self::assertSame(['bcmath', 'json', 'pcre', 'spl'], $selection->extensions);
self::assertSame([], $selection->features);
}
public function testDynamicFunctionCallFallsBackToAllExtensions(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::DYNAMIC_CAPABILITIES, 'function-call');
$statistics->finish();
$selection = (new NanoExtensionSelector())->select($statistics, ['json', 'standard']);
self::assertTrue($selection->completeFallback);
self::assertSame(['json', 'standard'], $selection->extensions);
}
public function testDateRemainsWholeWhileStandardUsesFineGrainedFeatures(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
foreach (['date', 'gmdate', 'array_keys', 'trim'] as $function) {
$statistics->record(CompilationStatistics::FUNCTIONS, $function);
$statistics->record(CompilationStatistics::DIRECT_FUNCTIONS, $function);
}
$statistics->finish();
$selection = (new NanoExtensionSelector())->select($statistics, ['date', 'standard']);
self::assertSame(['date'], $selection->extensions);
self::assertSame(
['standard.array', 'standard.string'],
$selection->features,
);
}
public function testZendDispatchedDateFunctionSelectsTheFullExtension(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'date_create');
$statistics->record(CompilationStatistics::RUNTIME_FUNCTIONS, 'date_create');
$statistics->finish();
$selection = (new NanoExtensionSelector())->select($statistics, ['date']);
self::assertSame(['date'], $selection->extensions);
self::assertSame([], $selection->features);
$composition = (new NanoSourceComposer())->compose(
sys_get_temp_dir() . '/typephp-nano-date-component-test',
'date_component_test',
false,
$statistics,
);
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/date/php_date.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/date/lib/timelib.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'src/std/datetime.cc'));
self::assertStringContainsString('date_module_entry', file_get_contents($composition['registry']));
}
public function testDirectWrapperStillSelectsItsProvidingExtension(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'json_encode');
$statistics->record(CompilationStatistics::DIRECT_FUNCTIONS, 'json_encode');
$statistics->finish();
$selection = (new NanoExtensionSelector())->select($statistics, ['json']);
self::assertSame(['json'], $selection->extensions);
self::assertSame([], $selection->features);
}
public function testNanoComposerAppliesStandardComponentSourcesAndDefines(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'array_keys');
$statistics->record(CompilationStatistics::DIRECT_FUNCTIONS, 'array_keys');
$statistics->finish();
$composition = (new NanoSourceComposer())->compose(
sys_get_temp_dir() . '/typephp-nano-component-test',
'component_test',
false,
$statistics,
);
self::assertContains('PHP_NANO_SELECTIVE=1', $composition['defines']);
self::assertContains('PHP_NANO_STANDARD_ARRAY=1', $composition['defines']);
self::assertContains('PHP_NANO_STANDARD_CORE=1', $composition['defines']);
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/array.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/math.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/string.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/html.c'));
self::assertFalse($this->containsSource($composition['packageSources'], 'ext/standard/base64.c'));
self::assertFalse($this->containsSource($composition['packageSources'], 'ext/date/php_date.c'));
$registry = file_get_contents($composition['registry']);
self::assertStringContainsString('basic_functions_module', $registry);
self::assertStringContainsString('random_module_entry', $registry);
self::assertStringNotContainsString('date_module_entry', $registry);
}
public function testNanoComposerAlwaysIncludesSharedStandardSources(): void
{
$statistics = new CompilationStatistics();
$statistics->begin();
$statistics->record(CompilationStatistics::FUNCTIONS, 'json_encode');
$statistics->record(CompilationStatistics::DIRECT_FUNCTIONS, 'json_encode');
$statistics->finish();
$composition = (new NanoSourceComposer())->compose(
sys_get_temp_dir() . '/typephp-nano-shared-standard-test',
'shared_standard_test',
false,
$statistics,
);
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/array.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/string.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/html.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/url.c'));
self::assertTrue($this->containsSource($composition['packageSources'], 'ext/standard/scanf.c'));
self::assertNotContains('PHP_NANO_STANDARD_ARRAY=1', $composition['defines']);
self::assertNotContains('PHP_NANO_STANDARD_STRING=1', $composition['defines']);
self::assertNotContains('PHP_NANO_STANDARD_ENCODING=1', $composition['defines']);
}
/** @param list<string> $sources */
private function containsSource(array $sources, string $suffix): bool
{
foreach ($sources as $source) {
if (str_ends_with(str_replace('\\', '/', $source), $suffix)) {
return true;
}
}
return false;
}
}

@ -0,0 +1,73 @@
<?php
namespace TypePhp\Analysis;
/**
* Usage information collected while TypePHP emits the final C++ program.
*
* This collector is intentionally independent from Nano. Build modules may
* reuse it for dependency selection, diagnostics, or compilation reports.
*/
final class CompilationStatistics
{
public const string FUNCTIONS = 'functions';
public const string DIRECT_FUNCTIONS = 'direct-functions';
public const string RUNTIME_FUNCTIONS = 'runtime-functions';
public const string CLASSES = 'classes';
public const string TYPES = 'types';
public const string DYNAMIC_CAPABILITIES = 'dynamic-capabilities';
/** @var array<string, array<string, int>> */
private array $counters = [];
private bool $collecting = false;
public function begin(): void
{
$this->counters = [];
$this->collecting = true;
}
public function finish(): void
{
$this->collecting = false;
}
public function isCollecting(): bool
{
return $this->collecting;
}
public function record(string $category, string $name): void
{
if (!$this->collecting || $category === '' || $name === '') {
return;
}
$this->counters[$category][$name] = ($this->counters[$category][$name] ?? 0) + 1;
}
/** @return array<string, int> */
public function get(string $category): array
{
$values = $this->counters[$category] ?? [];
ksort($values, SORT_STRING);
return $values;
}
public function has(string $category, string $name): bool
{
return isset($this->counters[$category][$name]);
}
/** @return array<string, array<string, int>> */
public function all(): array
{
$result = $this->counters;
ksort($result, SORT_STRING);
foreach ($result as &$values) {
ksort($values, SORT_STRING);
}
unset($values);
return $result;
}
}

@ -0,0 +1,22 @@
<?php
namespace TypePhp\Build;
/** A selectively composable part of a Composer native package. */
final readonly class ComposerNativeComponent
{
/**
* @param list<string> $sources
* @param list<string> $defines
* @param list<string> $requires
*/
public function __construct(
public string $name,
public string $extension,
public array $sources,
public array $defines,
public array $requires,
public ?string $moduleEntry,
) {
}
}

@ -11,6 +11,7 @@ final readonly class ComposerNativePackage
/**
* @param list<string> $includeDirs
* @param list<string> $sources
* @param array<string, ComposerNativeComponent> $components
*/
private function __construct(
public string $name,
@ -21,6 +22,7 @@ final readonly class ComposerNativePackage
public int $cxxStandard,
public array $includeDirs,
public array $sources,
public array $components,
public ?string $extensionName,
public ?string $extensionModuleEntry,
) {
@ -150,6 +152,65 @@ final readonly class ComposerNativePackage
}
}
$components = [];
foreach (($native['components'] ?? []) as $componentName => $component) {
if (!is_string($componentName)
|| preg_match('/^[a-z][a-z0-9_.-]*$/', $componentName) !== 1
|| !is_array($component)) {
throw new RuntimeException("Invalid native component metadata in `{$package}`");
}
$componentExtension = $component['extension'] ?? strstr($componentName, '.', true);
if ($componentExtension === false) {
$componentExtension = $componentName;
}
if (!is_string($componentExtension)
|| preg_match('/^[a-z][a-z0-9_]*$/', $componentExtension) !== 1) {
throw new RuntimeException("Invalid extension name for component `{$componentName}`");
}
$componentSourceEntries = $component['sources'] ?? [];
if (!is_array($componentSourceEntries)) {
throw new RuntimeException(
"Invalid source list in component `{$package}:{$componentName}`"
);
}
$componentSources = $componentSourceEntries === []
? []
: self::resolveEntries(
$root,
$componentSourceEntries,
false,
"{$package}:{$componentName}",
);
foreach ($componentSources as $source) {
if (!in_array($source, $sources, true)) {
throw new RuntimeException(
"Component `{$package}:{$componentName}` owns a source not published by the package"
);
}
}
$defines = self::stringList($component['defines'] ?? [], 'define', $package, $componentName);
foreach ($defines as $define) {
if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*(?:=.*)?$/', $define) !== 1) {
throw new RuntimeException("Invalid define in component `{$package}:{$componentName}`");
}
}
$requires = self::stringList($component['requires'] ?? [], 'requirement', $package, $componentName);
$moduleEntry = $component['module-entry'] ?? null;
if ($moduleEntry !== null
&& (!is_string($moduleEntry)
|| preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $moduleEntry) !== 1)) {
throw new RuntimeException("Invalid module entry in component `{$package}:{$componentName}`");
}
$components[$componentName] = new ComposerNativeComponent(
$componentName,
$componentExtension,
$componentSources,
$defines,
$requires,
$moduleEntry,
);
}
return new self(
$package,
$root,
@ -159,11 +220,30 @@ final readonly class ComposerNativePackage
$cxxStandard,
self::resolveEntries($root, $native['include-dirs'] ?? null, true, $package),
$sources,
$components,
$extensionName,
$extensionModuleEntry,
);
}
/** @return list<string> */
private static function stringList(
mixed $values,
string $label,
string $package,
string $component,
): array {
if (!is_array($values)) {
throw new RuntimeException("Invalid {$label} list in component `{$package}:{$component}`");
}
foreach ($values as $value) {
if (!is_string($value) || $value === '') {
throw new RuntimeException("Invalid {$label} in component `{$package}:{$component}`");
}
}
return array_values(array_unique($values));
}
/**
* Composer installs all three core packages as siblings below vendor/swoole.
* The same layout is used by the monorepo checkout, where a package may not

@ -0,0 +1,20 @@
<?php
namespace TypePhp\Build;
/** Result of resolving compiler usage to php-nano extension components. */
final readonly class NanoExtensionSelection
{
/**
* @param list<string> $extensions
* @param list<string> $features Fine-grained capabilities within large extensions.
* @param list<string> $reasons
*/
public function __construct(
public array $extensions,
public bool $completeFallback,
public array $features = [],
public array $reasons = [],
) {
}
}

@ -0,0 +1,401 @@
<?php
namespace TypePhp\Build;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\Type;
/**
* Converts generic compiler usage statistics into extension-level Nano needs.
*
* Source ownership remains php-nano metadata. This module only identifies
* extension names and computes their dependency closure.
*/
final class NanoExtensionSelector
{
/** @var array<string, string> */
private const array EXTENSION_NAMES = [
'standard' => 'standard',
'date' => 'date',
'hash' => 'hash',
'json' => 'json',
'pcre' => 'pcre',
'random' => 'random',
'reflection' => 'reflection',
'spl' => 'spl',
'filter' => 'filter',
'bcmath' => 'bcmath',
];
/** @var array<string, list<string>> */
private const array DEPENDENCIES = [
'spl' => ['json'],
];
/** @var array<string, string> */
private const array FUNCTION_OVERRIDES = [
'std::bigint' => 'bcmath',
'std::bigfloat' => 'bcmath',
'std::decimal' => 'bcmath',
];
/** @var array<string, string> */
private const array STANDARD_DIRECT_FEATURES = [
'count' => 'standard.array',
'sizeof' => 'standard.array',
'in_array' => 'standard.array',
'array_search' => 'standard.array',
'array_key_first' => 'standard.array',
'array_key_last' => 'standard.array',
'strlen' => 'standard.string',
'trim' => 'standard.string',
'ltrim' => 'standard.string',
'rtrim' => 'standard.string',
'strtolower' => 'standard.string',
'strtoupper' => 'standard.string',
'lcfirst' => 'standard.string',
'ucfirst' => 'standard.string',
'ucwords' => 'standard.string',
'explode' => 'standard.string',
'implode' => 'standard.string',
'strpos' => 'standard.string',
'stripos' => 'standard.string',
'strrpos' => 'standard.string',
'strstr' => 'standard.string',
'stristr' => 'standard.string',
'substr' => 'standard.string',
'dirname' => 'standard.string',
'basename' => 'standard.string',
'is_dir' => 'standard.filesystem',
'is_file' => 'standard.filesystem',
'file_exists' => 'standard.filesystem',
'realpath' => 'standard.filesystem',
'round' => 'standard.math',
'md5' => 'standard.hash',
'sha1' => 'standard.hash',
'version_compare' => 'standard.misc',
'print_r' => 'standard.misc',
'uniqid' => 'standard.misc',
'parse_str' => 'standard.misc',
'class_exists' => 'standard.core',
'interface_exists' => 'standard.core',
'trait_exists' => 'standard.core',
'enum_exists' => 'standard.core',
'function_exists' => 'standard.core',
'method_exists' => 'standard.core',
'property_exists' => 'standard.core',
'is_a' => 'standard.core',
'is_subclass_of' => 'standard.core',
'defined' => 'standard.core',
'define' => 'standard.core',
'get_parent_class' => 'standard.core',
];
/** @var array<string, string> */
private const array STANDARD_FUNCTION_FEATURES = [
'compact' => 'standard.array',
'current' => 'standard.array',
'end' => 'standard.array',
'key' => 'standard.array',
'max' => 'standard.array',
'min' => 'standard.array',
'next' => 'standard.array',
'pos' => 'standard.array',
'prev' => 'standard.array',
'range' => 'standard.array',
'reset' => 'standard.array',
'sort' => 'standard.array',
'rsort' => 'standard.array',
'asort' => 'standard.array',
'arsort' => 'standard.array',
'ksort' => 'standard.array',
'krsort' => 'standard.array',
'usort' => 'standard.array',
'uasort' => 'standard.array',
'uksort' => 'standard.array',
'base64_encode' => 'standard.encoding',
'base64_decode' => 'standard.encoding',
'htmlspecialchars' => 'standard.encoding',
'htmlspecialchars_decode' => 'standard.encoding',
'html_entity_decode' => 'standard.encoding',
'htmlentities' => 'standard.encoding',
'get_html_translation_table' => 'standard.encoding',
'parse_url' => 'standard.encoding',
'urlencode' => 'standard.encoding',
'urldecode' => 'standard.encoding',
'rawurlencode' => 'standard.encoding',
'rawurldecode' => 'standard.encoding',
'chdir' => 'standard.filesystem',
'chroot' => 'standard.filesystem',
'closedir' => 'standard.filesystem',
'copy' => 'standard.filesystem',
'disk_free_space' => 'standard.filesystem',
'disk_total_space' => 'standard.filesystem',
'getcwd' => 'standard.filesystem',
'glob' => 'standard.filesystem',
'mkdir' => 'standard.filesystem',
'opendir' => 'standard.filesystem',
'pathinfo' => 'standard.filesystem',
'readdir' => 'standard.filesystem',
'rename' => 'standard.filesystem',
'rewinddir' => 'standard.filesystem',
'rmdir' => 'standard.filesystem',
'scandir' => 'standard.filesystem',
'stat' => 'standard.filesystem',
'lstat' => 'standard.filesystem',
'tempnam' => 'standard.filesystem',
'tmpfile' => 'standard.filesystem',
'unlink' => 'standard.filesystem',
'fprintf' => 'standard.format',
'printf' => 'standard.format',
'scanf' => 'standard.format',
'sprintf' => 'standard.format',
'sscanf' => 'standard.format',
'vfprintf' => 'standard.format',
'vprintf' => 'standard.format',
'vsprintf' => 'standard.format',
'md5' => 'standard.hash',
'md5_file' => 'standard.hash',
'sha1' => 'standard.hash',
'sha1_file' => 'standard.hash',
'phpcredits' => 'standard.info',
'phpinfo' => 'standard.info',
'phpversion' => 'standard.info',
'php_ini_loaded_file' => 'standard.info',
'php_ini_scanned_files' => 'standard.info',
'php_sapi_name' => 'standard.info',
'php_uname' => 'standard.info',
'abs' => 'standard.math',
'ceil' => 'standard.math',
'floor' => 'standard.math',
'fmod' => 'standard.math',
'fdiv' => 'standard.math',
'fpow' => 'standard.math',
'intdiv' => 'standard.math',
'number_format' => 'standard.math',
'pi' => 'standard.math',
'pow' => 'standard.math',
'sqrt' => 'standard.math',
'hrtime' => 'standard.misc',
'microtime' => 'standard.misc',
'gettimeofday' => 'standard.misc',
'sleep' => 'standard.misc',
'usleep' => 'standard.misc',
'time_nanosleep' => 'standard.misc',
'time_sleep_until' => 'standard.misc',
'uniqid' => 'standard.misc',
'version_compare' => 'standard.misc',
'bin2hex' => 'standard.string',
'chr' => 'standard.string',
'explode' => 'standard.string',
'hex2bin' => 'standard.string',
'implode' => 'standard.string',
'join' => 'standard.string',
'nl2br' => 'standard.string',
'ord' => 'standard.string',
'parse_str' => 'standard.string',
'strrev' => 'standard.string',
'wordwrap' => 'standard.string',
'boolval' => 'standard.type',
'doubleval' => 'standard.type',
'floatval' => 'standard.type',
'get_debug_type' => 'standard.type',
'gettype' => 'standard.type',
'intval' => 'standard.type',
'settype' => 'standard.type',
'strval' => 'standard.type',
'debug_zval_dump' => 'standard.var',
'memory_get_peak_usage' => 'standard.var',
'memory_get_usage' => 'standard.var',
'memory_reset_peak_usage' => 'standard.var',
'print_r' => 'standard.var',
'serialize' => 'standard.var',
'unserialize' => 'standard.var',
'var_dump' => 'standard.var',
'var_export' => 'standard.var',
'constant' => 'standard.core',
'ini_alter' => 'standard.core',
'ini_get' => 'standard.core',
'ini_get_all' => 'standard.core',
'ini_parse_quantity' => 'standard.core',
'ini_restore' => 'standard.core',
'ini_set' => 'standard.core',
];
/** @var array<string, string> */
private const array STANDARD_CLASS_FEATURES = [
'__php_incomplete_class' => 'standard.var',
'roundingmode' => 'standard.math',
'sortdirection' => 'standard.array',
];
/**
* @param list<string> $availableExtensions
*/
public function select(
CompilationStatistics $statistics,
?array $availableExtensions = null,
): NanoExtensionSelection {
$availableExtensions ??= array_values(self::EXTENSION_NAMES);
$available = [];
foreach ($availableExtensions as $extension) {
$available[strtolower($extension)] = true;
}
ksort($available, SORT_STRING);
$dynamic = array_keys($statistics->get(CompilationStatistics::DYNAMIC_CAPABILITIES));
if ($dynamic !== []) {
return new NanoExtensionSelection(
array_keys($available),
true,
[],
array_map(
static fn(string $capability): string => "dynamic capability: {$capability}",
$dynamic,
),
);
}
$selected = [];
$features = [];
foreach (array_keys($statistics->get(CompilationStatistics::RUNTIME_FUNCTIONS)) as $function) {
$owner = self::FUNCTION_OVERRIDES[strtolower($function)]
?? $this->functionExtension($function);
if ($owner === 'standard') {
$feature = $this->standardFeature($function);
if ($feature === null) {
$selected['standard'] = true;
} else {
$features[$feature] = true;
}
continue;
}
if ($owner !== null && isset($available[$owner])) {
$selected[$owner] = true;
}
}
foreach (array_keys($statistics->get(CompilationStatistics::DIRECT_FUNCTIONS)) as $function) {
$owner = $this->functionExtension($function);
if ($owner === 'date') {
// Common date helpers are coupled to timelib and the DateTime
// implementation, so date remains one composition unit.
$selected['date'] = true;
continue;
}
if ($owner === 'standard') {
$feature = $this->standardFeature($function);
if ($feature === null) {
$selected['standard'] = true;
} else {
$features[$feature] = true;
}
continue;
}
if ($owner !== null && isset($available[$owner])) {
$selected[$owner] = true;
}
}
foreach (array_keys($statistics->get(CompilationStatistics::FUNCTIONS)) as $function) {
$owner = self::FUNCTION_OVERRIDES[strtolower($function)] ?? null;
if ($owner !== null && isset($available[$owner])) {
$selected[$owner] = true;
}
}
foreach (array_keys($statistics->get(CompilationStatistics::CLASSES)) as $class) {
$owner = $this->classExtension($class);
if ($owner === 'standard') {
$feature = self::STANDARD_CLASS_FEATURES[strtolower(ltrim($class, '\\'))] ?? null;
if ($feature === null) {
$selected['standard'] = true;
} else {
$features[$feature] = true;
}
continue;
}
if ($owner !== null && isset($available[$owner])) {
$selected[$owner] = true;
}
}
foreach ([Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL] as $type) {
if ($statistics->has(CompilationStatistics::TYPES, $type)
&& isset($available['bcmath'])
) {
$selected['bcmath'] = true;
}
}
$pending = array_keys($selected);
while (($extension = array_pop($pending)) !== null) {
foreach (self::DEPENDENCIES[$extension] ?? [] as $dependency) {
if (isset($available[$dependency]) && !isset($selected[$dependency])) {
$selected[$dependency] = true;
$pending[] = $dependency;
}
}
}
ksort($selected, SORT_STRING);
ksort($features, SORT_STRING);
return new NanoExtensionSelection(array_keys($selected), false, array_keys($features));
}
private function standardFeature(string $function): ?string
{
$function = strtolower($function);
$feature = self::STANDARD_DIRECT_FEATURES[$function]
?? self::STANDARD_FUNCTION_FEATURES[$function]
?? null;
if ($feature !== null) {
return $feature;
}
if (str_starts_with($function, 'array_')) {
return 'standard.array';
}
if (str_starts_with($function, 'str_') || str_starts_with($function, 'substr_')) {
return 'standard.string';
}
if (str_starts_with($function, 'file') || str_starts_with($function, 'stream_')) {
return 'standard.filesystem';
}
if (str_starts_with($function, 'is_')) {
return in_array($function, ['is_dir', 'is_file', 'is_link', 'is_readable', 'is_writable', 'is_writeable', 'is_executable'], true)
? 'standard.filesystem'
: 'standard.type';
}
return null;
}
private function functionExtension(string $name): ?string
{
if (!function_exists($name)) {
return null;
}
try {
return $this->normalizeExtension((new ReflectionFunction($name))->getExtensionName());
} catch (ReflectionException) {
return null;
}
}
private function classExtension(string $name): ?string
{
if (!class_exists($name, false) && !interface_exists($name, false)) {
return null;
}
try {
return $this->normalizeExtension((new ReflectionClass($name))->getExtensionName());
} catch (ReflectionException) {
return null;
}
}
private function normalizeExtension(string|false $extension): ?string
{
if (!is_string($extension)) {
return null;
}
return self::EXTENSION_NAMES[strtolower($extension)] ?? null;
}
}

@ -3,6 +3,7 @@
namespace TypePhp\Build;
use RuntimeException;
use TypePhp\Analysis\CompilationStatistics;
/** Resolves the Composer-provided runtime sources used by a Nano build. */
final class NanoSourceComposer
@ -12,10 +13,17 @@ final class NanoSourceComposer
* packages: list<ComposerNativePackage>,
* packageSources: list<string>,
* includeDirs: list<string>,
* registry: string
* defines: list<string>,
* registry: string,
* selection: NanoExtensionSelection|null
* }
*/
public function compose(string $buildDir, string $targetName, bool $registerProject): array
public function compose(
string $buildDir,
string $targetName,
bool $registerProject,
?CompilationStatistics $statistics = null,
): array
{
$runtime = ComposerNativePackage::load('swoole/php-nano');
$phpx = ComposerNativePackage::load('swoole/phpx');
@ -34,8 +42,16 @@ final class NanoSourceComposer
}
$packages = array_values($packagesByName);
$selection = $statistics === null
? null
: (new NanoExtensionSelector())->select($statistics);
$activeComponents = $selection === null
? null
: $this->selectComponents($packages, $selection);
$packageSources = [];
$includeDirs = [];
$defines = [];
foreach ($packages as $package) {
if ($package->abi !== $runtime->abi) {
throw new RuntimeException(
@ -43,29 +59,100 @@ final class NanoSourceComposer
. "swoole/php-nano={$runtime->abi}"
);
}
array_push($packageSources, ...$package->sources);
if ($activeComponents === null || $package->components === []) {
array_push($packageSources, ...$package->sources);
} else {
$ownedSources = [];
foreach ($package->components as $component) {
foreach ($component->sources as $source) {
$ownedSources[$source] = true;
}
if (isset($activeComponents[$package->name][$component->name])) {
array_push($packageSources, ...$component->sources);
array_push($defines, ...$component->defines);
}
}
foreach ($package->sources as $source) {
if (!isset($ownedSources[$source])) {
$packageSources[] = $source;
}
}
}
array_push($includeDirs, ...$package->includeDirs);
}
if ($activeComponents !== null) {
$defines[] = 'PHP_NANO_SELECTIVE=1';
}
return [
'packages' => $packages,
'packageSources' => array_values(array_unique($packageSources)),
'includeDirs' => array_values(array_unique($includeDirs)),
'defines' => array_values(array_unique($defines)),
'selection' => $selection,
'registry' => $this->writeExtensionRegistry(
$buildDir,
$targetName,
$packages,
$registerProject,
$activeComponents,
),
];
}
/**
* @param list<ComposerNativePackage> $packages
* @return array<string, array<string, true>>
*/
private function selectComponents(
array $packages,
NanoExtensionSelection $selection,
): array {
$extensionSet = array_fill_keys($selection->extensions, true);
$featureSet = array_fill_keys($selection->features, true);
$componentsByName = [];
foreach ($packages as $package) {
foreach ($package->components as $component) {
$componentsByName[$component->name][] = [$package, $component];
}
}
$active = [];
$pending = [];
foreach ($packages as $package) {
foreach ($package->components as $component) {
if (isset($extensionSet[$component->extension])
|| isset($featureSet[$component->name])) {
$active[$package->name][$component->name] = true;
$pending[] = $component;
}
}
}
while (($component = array_pop($pending)) !== null) {
foreach ($component->requires as $requirement) {
if (!isset($componentsByName[$requirement])) {
throw new RuntimeException(
"Nano component `{$component->name}` requires unknown component `{$requirement}`"
);
}
foreach ($componentsByName[$requirement] ?? [] as [$package, $required]) {
if (!isset($active[$package->name][$required->name])) {
$active[$package->name][$required->name] = true;
$pending[] = $required;
}
}
}
}
return $active;
}
/** @param list<ComposerNativePackage> $packages */
private function writeExtensionRegistry(
string $buildDir,
string $targetName,
array $packages,
bool $registerProject,
?array $activeComponents,
): string {
if (!is_dir($buildDir) && !mkdir($buildDir, 0777, true) && !is_dir($buildDir)) {
throw new RuntimeException("Unable to create Nano build directory: {$buildDir}");
@ -78,25 +165,37 @@ final class NanoSourceComposer
$path = $buildDir . DIRECTORY_SEPARATOR . 'composer_extensions.cpp';
$declarations = [];
$entries = [];
foreach ($packages as $package) {
foreach ($package->components as $component) {
if (($activeComponents !== null
&& !isset($activeComponents[$package->name][$component->name]))
|| $component->moduleEntry === null) {
continue;
}
$moduleEntry = $component->moduleEntry;
$declarations[$moduleEntry] = "extern \"C\" zend_module_entry {$moduleEntry};";
$entries[$moduleEntry] = " &{$moduleEntry},";
}
}
foreach ($extensions as $extension) {
$moduleEntry = $extension->extensionModuleEntry;
$declarations[] = "extern \"C\" zend_module_entry {$moduleEntry};";
$entries[] = " &{$moduleEntry},";
$declarations[$moduleEntry] = "extern \"C\" zend_module_entry {$moduleEntry};";
$entries[$moduleEntry] = " &{$moduleEntry},";
}
if ($registerProject) {
$namespace = 'typephp_project_' . $targetName;
$moduleEntry = 'typephp_' . $targetName . '_module_entry';
$declarations[] = "namespace {$namespace} { extern zend_module_entry {$moduleEntry}; }";
$entries[] = " &{$namespace}::{$moduleEntry},";
$declarations[$moduleEntry] = "namespace {$namespace} { extern zend_module_entry {$moduleEntry}; }";
$entries[$moduleEntry] = " &{$namespace}::{$moduleEntry},";
}
$count = count($entries);
$storageSize = max(1, $count);
$contents = "#include <php_nano_extension.h>\n\n"
. implode("\n", $declarations) . "\n\n"
. implode("\n", array_values($declarations)) . "\n\n"
. "extern \"C\" zend_result php_nano_startup_composer_extensions() {\n"
. " static zend_module_entry *extensions[{$storageSize}] = {\n"
. implode("\n", $entries) . "\n"
. implode("\n", array_values($entries)) . "\n"
. " };\n"
. " return php_nano_startup_extensions(extensions, {$count});\n"
. "}\n\n"

@ -41,6 +41,7 @@ trait NativeCommandOptionsTrait
'PHPX_NANO=1',
'_POSIX_C_SOURCE=200809L',
);
array_push($userDefines, ...$this->nanoRuntimeDefines);
if ($this->isWasiTarget()) {
$userDefines[] = 'ZEND_MM_ERROR=0';
}

@ -39,6 +39,7 @@ final class NativeSourceProjectBuilder
$generatedSources = [];
$generatedIncludeDir = null;
$translator = null;
if ($project->phpSources !== []) {
$this->writeProgress(
'Generating C++ from ' . count($project->phpSources) . ' TypePHP source file(s)'
@ -59,6 +60,7 @@ final class NativeSourceProjectBuilder
$project->buildDir,
$this->projectSymbolName($project),
$project->phpSources !== [],
$translator?->getCompilationStatistics(),
);
$packages = $composition['packages'];
$extensionRegistry = $composition['registry'];
@ -73,6 +75,14 @@ final class NativeSourceProjectBuilder
$sources = array_values(array_unique($sources));
$includeDirs = array_values(array_unique($includeDirs));
$sourceCount = count($sources);
$selection = $composition['selection'];
if ($selection !== null) {
$selected = [...$selection->extensions, ...$selection->features];
$this->writeProgress(
'Nano capability selection: ' . ($selected === [] ? 'core only' : implode(', ', $selected))
. ($selection->completeFallback ? ' (complete fallback)' : ''),
);
}
$this->writeProgress(
"Resolved {$sourceCount} C/C++ source file(s) from " . count($packages) . ' Composer package(s)'
);
@ -99,6 +109,10 @@ final class NativeSourceProjectBuilder
'-DPHP_NANO=1',
'-DPHPX_NANO=1',
'-D_POSIX_C_SOURCE=200809L',
...array_map(
static fn(string $define): string => '-D' . $define,
$composition['defines'],
),
'-ffunction-sections',
'-fdata-sections',
// Package headers are passed with -isystem to suppress upstream

@ -334,9 +334,11 @@ trait SourcePipelineTrait
public function convert(array $files): array
{
$this->composeTraitDeclarations($files);
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
$this->compilationStatistics->begin();
$previousPhase = null;
try {
$this->composeTraitDeclarations($files);
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
// All declarations are now known. Lower declaration constant
// expressions before translating any function body so cache IDs
// are assigned exclusively in the convert phase.
@ -392,7 +394,10 @@ trait SourcePipelineTrait
return $sourceFiles;
} finally {
$this->restoreCompilerPhase($previousPhase);
if ($previousPhase !== null) {
$this->restoreCompilerPhase($previousPhase);
}
$this->compilationStatistics->finish();
}
}
}

@ -8,6 +8,7 @@
namespace TypePhp;
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\Build\PhpxLocator;
use League\CLImate\CLImate;
@ -529,6 +530,8 @@ class CompilerBase implements PropertyAccessContext
protected bool $nanoPolicyMode = false;
/** @var list<string> Include directories published by Nano Composer packages. */
protected array $nanoRuntimeIncludePaths = [];
/** @var list<string> */
protected array $nanoRuntimeDefines = [];
/** @var array<string, true> Package source files compiled into a Nano executable. */
protected array $nanoRuntimeSources = [];
/** Latest header timestamp used by the shared object-cache path. */
@ -653,6 +656,9 @@ class CompilerBase implements PropertyAccessContext
*/
protected array $classSubClasses = [];
/** Whole-program usage collected during the convert phase. */
protected CompilationStatistics $compilationStatistics;
public function __construct(string $rootPath)
{
$this->osType = PHP_OS_FAMILY;
@ -663,6 +669,7 @@ class CompilerBase implements PropertyAccessContext
$this->error('PHP 8.6.0 or later is not supported');
}
$this->rootPath = $rootPath;
$this->compilationStatistics = new CompilationStatistics();
$this->symbols = new SymbolRepository();
$this->setPhpVersion(self::DEFAULT_PHP_VERSION);
$this->printer = new PrettyPrinter\Standard();
@ -927,7 +934,38 @@ class CompilerBase implements PropertyAccessContext
public function getTypeFromZendType(string $type): string
{
return $this->zendTypeMap[$type] ?? self::PHP_RUNTIME_TYPE_MAP[$type] ?? Type::VAR;
$resolved = $this->zendTypeMap[$type] ?? self::PHP_RUNTIME_TYPE_MAP[$type] ?? Type::VAR;
$this->compilationStatistics->record(CompilationStatistics::TYPES, $resolved);
return $resolved;
}
public function getCompilationStatistics(): CompilationStatistics
{
return $this->compilationStatistics;
}
/** Record value types that survived into an emitted translation unit. */
protected function recordEmittedTypes(string $code): void
{
foreach ([
Type::VAR,
Type::BOOL,
Type::INT,
Type::FLOAT,
Type::STR,
Type::ARRAY,
Type::OBJECT,
Type::RESOURCE,
Type::STREAM,
Type::BIGINT,
Type::BIGFLOAT,
Type::DECIMAL,
Type::BOX,
] as $type) {
if (str_contains($code, $type)) {
$this->compilationStatistics->record(CompilationStatistics::TYPES, $type);
}
}
}
public function getObjectType(string $object): string
@ -1537,6 +1575,10 @@ class CompilerBase implements PropertyAccessContext
protected function getClassEntryPtr(string $className): string
{
$this->compilationStatistics->record(
CompilationStatistics::CLASSES,
ltrim($className, '\\'),
);
$id = $this->getClassId($className);
$persistent = isset($this->persistentClassMap[$className]);
$helper = $persistent ? 'get_persistent_class' : 'get_class';

@ -7,6 +7,7 @@
namespace TypePhp\Parser;
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\Type;
use PhpParser\Node;
@ -104,12 +105,20 @@ trait FunctionCallTrait
}
if ($this->isVarExpr($expr->name)) {
$this->compilationStatistics->record(
CompilationStatistics::DYNAMIC_CAPABILITIES,
'function-call',
);
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;
$name = '';
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name);
$globalName = ltrim($name, '\\');
$this->compilationStatistics->record(
CompilationStatistics::FUNCTIONS,
strtolower($globalName),
);
$namedExit = $this->parseNamedExitMessageCall($globalName, $expr);
if ($namedExit !== null) {
return $namedExit;
@ -185,14 +194,30 @@ trait FunctionCallTrait
$this->checkInternalFunctionArgCount($name, $expr);
$code = $this->parseFuncCallWithOptimizer($name, $expr);
if ($code !== false) {
// Constant folding and native container operations do not
// retain the PHP function. Record only emitted stdlib calls.
if (str_contains($code, 'php::fn::')) {
$this->compilationStatistics->record(
CompilationStatistics::DIRECT_FUNCTIONS,
strtolower($globalName),
);
}
return $code;
}
$this->compilationStatistics->record(
CompilationStatistics::RUNTIME_FUNCTIONS,
strtolower($globalName),
);
$placeHolder = $this->identifierToStr($expr->name);
$fn = $this->getFuncPtr($name);
if ($this->debug) {
$this->context->beforeStmtLines[] = $this->formatCppLineComment('Func Call: ', $name . '()');
}
} else {
$this->compilationStatistics->record(
CompilationStatistics::DYNAMIC_CAPABILITIES,
'function-call',
);
$tmpVar = $this->addTmpVar(Type::VAR);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $this->parseExpr($expr->name) . ';';
$placeHolder = $fn = $tmpVar;

@ -2,6 +2,7 @@
namespace TypePhp\Parser;
use TypePhp\Analysis\CompilationStatistics;
use TypePhp\Type;
use TypePhp\CompilerBase;
@ -600,6 +601,17 @@ trait UniversalMethodCall
*/
protected function parseUniversalMethodCall(Node\Expr\MethodCall $expr, string $receiver, string $method, array $def, bool $isVar = true): ?string
{
if ($def['handler'] === 'php_fn') {
$function = strtolower($def['fn']);
$this->compilationStatistics->record(CompilationStatistics::FUNCTIONS, $function);
$this->compilationStatistics->record(CompilationStatistics::DIRECT_FUNCTIONS, $function);
} elseif ($def['handler'] === 'cpp_fn'
&& str_starts_with($def['fn'], 'php::fn::')
) {
$function = strtolower(substr($def['fn'], strlen('php::fn::')));
$this->compilationStatistics->record(CompilationStatistics::FUNCTIONS, $function);
$this->compilationStatistics->record(CompilationStatistics::DIRECT_FUNCTIONS, $function);
}
if ($this->isWasiTarget()) {
if ($def['handler'] === 'php_fn') {
$this->assertWasiFunctionSupported($expr, $def['fn']);

@ -721,12 +721,17 @@ class Translator extends Preprocessor
public function convertFile(string $file): ?string
{
$ownsStatisticsSession = !$this->compilationStatistics->isCollecting();
if ($ownsStatisticsSession) {
$this->compilationStatistics->begin();
}
// Public embedding/test callers may prepare files directly instead of
// using SourcePipelineTrait::prepare(). Preserve the same explicit
// prepare -> Trait composition -> convert ordering for that API.
$this->composeTraitDeclarations(array_keys($this->preparedFileAsts));
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
$previousPhase = null;
try {
$this->composeTraitDeclarations(array_keys($this->preparedFileAsts));
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
if (!$this->declarationExpressionsFinalized) {
$this->finalizeDeclarationExpressions(array_keys($this->preparedFileAsts));
}
@ -737,6 +742,7 @@ class Translator extends Preprocessor
while (true) {
try {
$cppCode = $this->doConvert($phpCode);
$this->recordEmittedTypes($cppCode);
$cppFile = $this->getCppFile($file);
if ($cppCode === '') {
$this->removeEmptyTranslationUnitArtifacts($cppFile);
@ -752,7 +758,12 @@ class Translator extends Preprocessor
}
}
} finally {
$this->restoreCompilerPhase($previousPhase);
if ($previousPhase !== null) {
$this->restoreCompilerPhase($previousPhase);
}
if ($ownsStatisticsSession) {
$this->compilationStatistics->finish();
}
}
}
@ -2008,8 +2019,10 @@ CODE;
$this->getBuildDir(),
$this->targetName,
true,
$this->getCompilationStatistics(),
);
$this->nanoRuntimeIncludePaths = $composition['includeDirs'];
$this->nanoRuntimeDefines = $composition['defines'];
$this->nanoRuntimeSources = array_fill_keys($composition['packageSources'], true);
$this->nanoRuntimeHeaderMtime = $this->latestNanoHeaderMtime(
$this->nanoRuntimeIncludePaths,

Loading…
Cancel
Save