- 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 newmaster
parent
7726f49cd7
commit
a1782233ca
15 changed files with 1032 additions and 16 deletions
@ -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, |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue