refactor(compiler): enhance type checking and improve code structure

- Add phpstan assertions for better type safety in AstNodeType and CallArgumentGenerator
- Replace null check with direct condition in ClassFieldSelection to streamline validation
- Remove unused FunctionLike import and add GlobalConstantDef import in CompilerBase
- Convert CompilerBase to abstract class with abstract methods for better structure
- Update constants array type hint from ConstantDef to GlobalConstantDef
- Replace class inheritance tracking with SymbolRepository for compiler symbols
- Change isScalarInt parameter type from Expr to Node and update return type
- Update getFunctionName parameter type to specific node types instead of FunctionLike
- Refactor parseScalar method to use instanceof checks instead of getType() switch
- Replace string comparison with instanceof check in getComment method
- Change parseNumericIdentifier condition to use instanceof instead of getType()
- Fix argument count comparison in function call validation logic
- Use getRawArgs() method for accessing call arguments in
master
韩天峰 2 weeks ago
parent 0ad695ee63
commit d8267c2f00
  1. 13
      .github/workflows/windows-build.yml
  2. 4
      README-CN.md
  3. 7
      README.md
  4. 43
      phpunit/src/CompilerBaseApiTest.php
  5. 12
      src/Build/NanoBuildBackend.php
  6. 2
      src/Build/ResourceCompilationTrait.php
  7. 6
      src/Build/SourcePipelineTrait.php
  8. 74
      src/CompilerBase.php
  9. 1
      src/Generator/CallArgumentGenerator.php
  10. 5
      src/Immutable/ImmutableSupportTrait.php
  11. 2
      src/Metadata/Constants.php
  12. 7
      src/NativeClass/NativeGlobalTypeResolver.php
  13. 1
      src/Parser/AstNodeType.php
  14. 16
      src/Parser/FunctionCallTrait.php
  15. 21
      src/Parser/StdContainerTrait.php
  16. 1
      src/Parser/TypedArrayTrait.php
  17. 24
      src/Parser/UniversalMethodCall.php
  18. 104
      src/Preprocessor.php
  19. 24
      src/Resolver/DeclarationSymbolTrait.php
  20. 23
      src/Resolver/NameResolutionTrait.php
  21. 6
      src/Testing/TestCoverageAnalyzer.php
  22. 2
      src/Transform/ClassFieldSelection.php
  23. 6
      src/Transform/CompileTimeAttribute.php
  24. 2
      src/Transform/ConstantExpressionValidator.php
  25. 6
      src/Transform/GetterLowering.php
  26. 1
      src/Transform/NanoSyntaxValidationVisitor.php
  27. 12
      src/Transform/PropertyMethodLowering.php
  28. 6
      src/Transform/ValidateLowering.php
  29. 31
      src/Translator.php

@ -461,6 +461,19 @@ jobs:
throw "Windows Nano policy probe returned an unexpected error: $policyOutput"
}
# A GUI-subsystem Nano application uses the same generated entry and
# DLL backend, but must also link without a console window.
$nanoGuiExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_nano_gui_smoke.exe'
$env:PHPRC = Join-Path $env:PHP_HOME 'php.ini'
& .\tpc.exe tests\windows\smoke\project.yml --nano --no-console --output $nanoGuiExe --job 1 --no-progress
if ($LASTEXITCODE -ne 0) {
throw "Windows Nano GUI project compilation failed with exit code $LASTEXITCODE"
}
Remove-Item Env:PHPRC
if (-not (Test-Path $nanoGuiExe)) {
throw "Windows Nano GUI executable was not generated: $nanoGuiExe"
}
- name: Package tested Windows compiler
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh

@ -252,8 +252,8 @@ C++17 与 POSIX.1-2008,但依然不提供 socket、DNS、网络、远程 strea
所有平台的 `--nano` 都会拒绝 `eval`、`include`、`include_once`、`require`、
`require_once` 等 VM 入口以及匿名类。
Windows 的差异在于构建后端:即使指定 `--nano`,也仍走原有的宿主机编译、链接
流程,通过 import library 连接 `php.dll` 与 `phpx.dll`。Windows 不加载
Windows 通过独立构建后端支持 `mode: bin` 和 `mode: lib` 的 Nano 原生应用:它仍走
宿主机编译、链接流程,通过 import library 连接 `php.dll` 与 `phpx.dll`。Windows 不加载
`swoole/php-nano`、`swoole/phpx` 的源码清单,也不会把它们的 C/C++ 源文件加入
项目 `sources`。外部命令 API 与反引号语法依然会被拒绝;请求启动时还会从 Zend
函数表移除这些命令函数,避免变量函数或回调形式绕过编译期检查。

@ -287,9 +287,10 @@ that target are compile-time errors.
On every platform, `--nano` rejects the VM entry paths `eval`, `include`,
`include_once`, `require`, and `require_once`, as well as anonymous classes.
Windows uses a different build backend even when `--nano` is specified: it keeps
the existing host compile/link pipeline and connects to `php.dll` and `phpx.dll`
through their import libraries. It does not load the `swoole/php-nano` or
Windows supports Nano native applications in `mode: bin` and `mode: lib` through
a different build backend: it keeps the existing host compile/link pipeline and
connects to `php.dll` and `phpx.dll` through their import libraries. It does not
load the `swoole/php-nano` or
`swoole/phpx` source manifests, nor append their C/C++ files to project `sources`.
External-command APIs and backtick syntax are still rejected. Those command
functions are also removed from the Zend function table at request startup, so

@ -1344,9 +1344,46 @@ YAML);
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT);
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'--nano source composition does not support extension mode (-m ext)',
);
$this->expectExceptionMessage('--nano does not support extension mode (-m ext)');
$this->invokeMethod('applyCommandLineArguments');
}
public function testWindowsNanoUsesNativeDllBackendAndRejectsExtensionMode(): void
{
global $argv;
$argv = ['compiler.php', '--nano'];
$compiler = CompilerTest::create($this->testDir);
$reflection = new \ReflectionClass($compiler);
$platform = $reflection->getProperty('platform');
$platform->setAccessible(true);
$platform->setValue($compiler, new Windows());
$apply = $reflection->getMethod('applyCommandLineArguments');
$apply->setAccessible(true);
$apply->invoke($compiler);
self::assertTrue($compiler->isNanoPolicyMode());
self::assertFalse($compiler->isNanoMode());
$buildMode = $reflection->getProperty('buildMode');
$buildMode->setAccessible(true);
$buildMode->setValue($compiler, CompilerBase::BUILD_MODE_EXT);
$this->expectException(TestError::class);
$this->expectExceptionMessage('--nano does not support extension mode (-m ext)');
$apply->invoke($compiler);
}
public function testWindowsNanoRequiresCxx17(): void
{
$this->setPropertyValue('nanoPolicyMode', true);
$this->setPropertyValue('nanoMode', false);
$this->setPropertyValue('platform', new Windows());
$this->setPropertyValue('cxxStd', 'c++20');
$this->expectException(TestError::class);
$this->expectExceptionMessage('--nano requires the C++17 language standard');
$this->invokeMethod('applyCommandLineArguments');
}

@ -2,22 +2,22 @@
namespace TypePhp\Build;
/** Selects the runtime build backend used by the --nano policy mode. */
/** Selects the runtime build backend used by a --nano native application. */
final class NanoBuildBackend
{
/** Existing Windows host build linked through the PHP/PHPX import libraries. */
/** Windows native application linked through the PHP/PHPX import libraries. */
public const WINDOWS_DLL = 'windows-dll';
/** Composer package manifests whose C/C++ sources are compiled into the program. */
public const COMPOSER_SOURCES = 'composer-sources';
public static function forHost(string $osFamily): string
public static function forHost(string $platformName): string
{
return $osFamily === 'Windows' ? self::WINDOWS_DLL : self::COMPOSER_SOURCES;
return $platformName === 'Windows' ? self::WINDOWS_DLL : self::COMPOSER_SOURCES;
}
public static function composesRuntimeSources(string $osFamily): bool
public static function composesRuntimeSources(string $platformName): bool
{
return self::forHost($osFamily) === self::COMPOSER_SOURCES;
return self::forHost($platformName) === self::COMPOSER_SOURCES;
}
}

@ -105,7 +105,7 @@ trait ResourceCompilationTrait
}
$this->climate->green('Resource compiled: ' . $resFile);
} else {
$this->climate->warning('Resource files are only supported with MSVC backend on Windows');
$this->climate->out('Resource files are only supported with MSVC backend on Windows');
}
}
}

@ -358,7 +358,7 @@ trait SourcePipelineTrait
$progress = $this->startOpcodeProgress('Opcodes', $batch->pendingCount());
$blobs = $generator->compile(
$batch,
fn (string $file) => $this->climate->warning(
fn (string $file) => $this->climate->out(
'Skipping non-executable embedded PHP file: ' . $file,
),
fn (int $completed, int $total, string $file) => $this->updateOpcodeProgress(
@ -525,7 +525,7 @@ trait SourcePipelineTrait
$this->error('`sapi` containing cli requires an `entry` PHP file');
}
if ($this->sapiEntryConfiguredPath !== null && !$this->hasSapi('cli')) {
$this->climate->warning('`entry` is ignored because `sapi` does not contain cli');
$this->climate->out('`entry` is ignored because `sapi` does not contain cli');
$this->sapiEntryFile = null;
$this->sapiEntryConfiguredPath = null;
$this->sapiEntryConfiguredValue = null;
@ -699,7 +699,7 @@ trait SourcePipelineTrait
}
$this->error($detail);
}
$this->climate->warning($message['warning']);
$this->climate->out($message['warning']);
if (!empty($message['info'])) {
$this->climate->info($message['info']);
}

@ -14,7 +14,6 @@ use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\NodeAbstract;
use PhpParser\Parser;
use PhpParser\ParserFactory;
@ -39,6 +38,7 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ConstantDef;
use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\GlobalConstantDef;
use TypePhp\Entity\InterfaceDef;
use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef;
@ -103,8 +103,36 @@ use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
use TypePhp\TypeSystem\CompoundTypeDeclarationValidationTrait;
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
class CompilerBase implements PropertyAccessContext
abstract class CompilerBase implements PropertyAccessContext
{
abstract protected function genDefaultArgumentExpr(string $nativeName, int $argumentIndex): string;
abstract protected function getFunctionOptimizationAttribute(FunctionDef $function): string;
abstract protected function findClassMethodDef(
ClassDef $classDef,
string $methodName,
bool $includeAbstract = true,
): ?MethodDef;
abstract protected function validateMethodOverrideSignature(
NodeAbstract $v,
string $methodName,
MethodDef $childMethodDef,
MethodDef $parentMethodDef,
string $parentClass,
?string $childClass = null,
): void;
abstract public function getClassConstValue(
NodeAbstract $expr,
string $class,
string $name,
string $currentClass = '',
): mixed;
abstract protected function genArgumentDeclaration(ArgInfo $argInfo): string;
use CompositeTypeCheckerTrait;
use CompoundTypeDeclarationValidationTrait;
use CompilerDiagnosticTrait;
@ -619,7 +647,7 @@ class CompilerBase implements PropertyAccessContext
protected string $interface = '';
/**
* @var array<string, ConstantDef>
* @var array<string, GlobalConstantDef>
*/
protected array $constants = [];
@ -729,8 +757,7 @@ class CompilerBase implements PropertyAccessContext
protected array $classMethodOverride = [];
/**
* Stores all class inheritance relationships. Class names must be all lowercase.
* @var array<string, string>
* Stores compiler symbols collected during preprocessing.
*/
protected SymbolRepository $symbols;
@ -1026,9 +1053,10 @@ class CompilerBase implements PropertyAccessContext
}
}
public function isScalarInt(Expr $expr): bool
/** @phpstan-assert-if-true Node\Scalar\Int_ $expr */
public function isScalarInt(Node $expr): bool
{
return $expr instanceof Node\Scalar\LNumber;
return $expr instanceof Node\Scalar\Int_;
}
public function getLine($node): int
@ -1425,7 +1453,7 @@ class CompilerBase implements PropertyAccessContext
$this->namespace = '';
}
protected function getFunctionName(FunctionLike $v): string
protected function getFunctionName(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): string
{
if ($this->methodDef !== null && $this->classDef !== null) {
return $this->getNativeName(
@ -1899,14 +1927,13 @@ class CompilerBase implements PropertyAccessContext
protected function parseScalar(Node\Scalar $expr): string
{
$type = $expr->getType();
switch ($type) {
case 'Scalar_Int':
if ($expr instanceof Node\Scalar\Int_) {
if ($this->bigintTypes) {
return 'php::toBigInt(' . $expr->value . ')';
}
return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix();
case 'Scalar_Float':
}
if ($expr instanceof Node\Scalar\Float_) {
if ($this->isBigIntLiteral($expr)) {
return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')';
}
@ -1916,13 +1943,13 @@ class CompilerBase implements PropertyAccessContext
return 'php::toDecimal(' . $this->getLiteralString($clean) . ')';
}
return $this->parseScalarFloat($expr);
case 'Scalar_String':
return $expr->hasAttribute('noLiteralString') ? $this->getInlineString($expr->value) : $this->getLiteralString($expr->value);
default:
$this->unsupportedSyntax($expr);
break;
}
return '';
if ($expr instanceof Node\Scalar\String_) {
return $expr->hasAttribute('noLiteralString')
? $this->getInlineString($expr->value)
: $this->getLiteralString($expr->value);
}
$this->unsupportedSyntax($expr);
}
/**
@ -2090,7 +2117,7 @@ class CompilerBase implements PropertyAccessContext
protected function getComment(Node\Stmt $v, string $class): string
{
if ($class == 'Stmt_Expression') {
if ($v instanceof Node\Stmt\Expression) {
$class = 'Stmt_Expression(' . $v->expr->getType() . ')';
}
@ -2438,7 +2465,7 @@ class CompilerBase implements PropertyAccessContext
*/
protected function parseNumericIdentifier(NodeAbstract $expr): string
{
if ($expr->getType() === 'Scalar_String') {
if ($expr instanceof Node\Scalar\String_) {
if ($this->isFloatStr($expr->value)) {
return (string) floatval($expr->value);
}
@ -3161,7 +3188,7 @@ class CompilerBase implements PropertyAccessContext
$type = str_contains($name, '::') ? 'Method' : 'Function';
if ($argc < $funcDef->argCountRequired) {
$this->fatalError($expr, $type . ' `' . $name . '()` requires ' . $funcDef->argCountRequired . ' arguments, ' . $argc . ' given');
} elseif (!$funcDef->hasVariadicArg() and count($expr->args) > count($funcDef->argInfoList)) {
} elseif (!$funcDef->hasVariadicArg() and $argc > count($funcDef->argInfoList)) {
$this->fatalError($expr, $type . ' `' . $name . '()` accepts ' . count($funcDef->argInfoList) . ' arguments, ' . $argc . ' given');
}
}
@ -3286,11 +3313,12 @@ class CompilerBase implements PropertyAccessContext
$this->fatalError($expr, 'Method `' . $classDef->getNamespacedName() . '::' . $method . '()` is not accessible');
}
// A function-call placeholder, not a real function call.
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
$args = $expr->getRawArgs();
if (count($args) === 1 and $this->isPlaceholderExpr($args[0])) {
return false;
}
if ($checkArgs) {
$this->checkNativeCallArgs($expr, $methodDef->functionDef, $expr->args, $classDef->getNamespacedName() . '::' . $method);
$this->checkNativeCallArgs($expr, $methodDef->functionDef, $args, $classDef->getNamespacedName() . '::' . $method);
}
return $this->getNativeName($method, $classDef->namespace, $classDef->name);
}

@ -972,6 +972,7 @@ trait CallArgumentGenerator
return '&' . $tmpRef;
}
/** @phpstan-assert-if-true Node\Expr\MethodCall $expr */
protected function isToRefCall(NodeAbstract $expr): bool
{
return $this->isMethodCall($expr)

@ -183,8 +183,7 @@ trait ImmutableSupportTrait
}
$type = $this->detectTypeOfExpr($call->var);
$method = $call->name->toString();
$definition = self::UNIVERSAL_METHODS[$type][$method] ?? null;
if ($definition !== null && in_array($definition['handler'], self::MUTATING_HANDLERS, true)) {
if ($this->isUniversalMethodMutating($type, $method)) {
$this->fatalError(
$call,
"Cannot call mutating method `{$method}()` on immutable value `"
@ -331,7 +330,7 @@ trait ImmutableSupportTrait
if (!$staticallyResolved) {
return;
}
foreach ($call->args as $index => $argument) {
foreach ($call->getRawArgs() as $index => $argument) {
if ($argument instanceof Node\VariadicPlaceholder) {
continue;
}

@ -350,7 +350,7 @@ class Constants
* MSVC compiler warning suppression list.
* These warnings come from Windows SDK and PHP SDK headers and are compiler noise that does not affect functionality.
*
* @var array<string, string> key is the warning number, value is the description
* @var array<int, string> key is the warning number, value is the description
*/
public const array MSVC_SUPPRESSED_WARNINGS = [
'4244' => '类型转换可能丢失数据 (int -> smaller type)',

@ -12,6 +12,7 @@ use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\GlobalConstantDef;
final class NativeGlobalConstantVisitState
{
@ -51,7 +52,7 @@ final class NativeGlobalTypeResolver
/**
* @param array<string, ClassDef> $classes
* @param array<string, object> $constants
* @param array<string, GlobalConstantDef> $constants
*/
public function __construct(array $classes, array $constants = [])
{
@ -66,9 +67,7 @@ final class NativeGlobalTypeResolver
}
foreach ($constants as $constant) {
if (isset($constant->name) && is_string($constant->name)
&& isset($constant->valueExpr) && $constant->valueExpr instanceof Node\Expr
) {
if ($constant->valueExpr instanceof Node\Expr) {
$this->globalConstantExpressions[ltrim($constant->name, '\\')] = $constant->valueExpr;
}
}

@ -86,6 +86,7 @@ trait AstNodeType
return $expr instanceof Expr\FuncCall;
}
/** @phpstan-assert-if-true Expr\StaticCall $expr */
protected function isStdRefCall(Node $expr): bool
{
return $this->isStaticCall($expr)

@ -174,15 +174,18 @@ trait FunctionCallTrait
}
$directCall = clone $callable;
$directCall->args = [new Node\Arg(new Variable($value))];
$args = [new Node\Arg(new Variable($value))];
if ($directCall instanceof Expr\FuncCall) {
$directCall->args = $args;
return $this->parseFuncCall($directCall);
}
if ($directCall instanceof Expr\StaticCall) {
$directCall->args = $args;
return $this->parseStaticCall($directCall);
}
if ($directCall instanceof Expr\MethodCall) {
$directCall->args = $args;
return $this->parseMethodCall($directCall);
}
@ -437,16 +440,17 @@ trait FunctionCallTrait
*/
protected function parseAnyCompileTimeCall(CallLike $expr): string
{
if (count($expr->args) === 0) {
$args = $expr->getRawArgs();
if (count($args) === 0) {
return self::VALUE_NULL;
}
if (count($expr->args) !== 1
|| !$expr->args[0] instanceof Node\Arg
|| $expr->args[0]->unpack
if (count($args) !== 1
|| !$args[0] instanceof Node\Arg
|| $args[0]->unpack
) {
$this->fatalError($expr, 'The std::any function expects zero or one non-unpacked argument');
}
$value = $expr->args[0]->value;
$value = $args[0]->value;
if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) {
$this->fatalError(
$value,

@ -153,8 +153,8 @@ trait StdContainerTrait
}
$dimensions = [];
foreach ($expr->items as $item) {
if ($item === null || $item->key !== null || $item->unpack || !$this->isScalarInt($item->value)) {
$this->fatalError($item ?? $expr, 'StdArray dimensions must be a positional array of integer literals');
if ($item->key !== null || $item->unpack || !$this->isScalarInt($item->value)) {
$this->fatalError($item, 'StdArray dimensions must be a positional array of integer literals');
}
$dimensions[] = $item->value->value;
}
@ -183,6 +183,7 @@ trait StdContainerTrait
'vector' => Type::STD_VECTOR,
'map' => Type::STD_MAP,
'ordered_map' => Type::STD_ORDERED_MAP,
default => throw new \LogicException('Unknown std container kind: ' . (string) $info['kind']),
};
if ($type === Type::STD_ARRAY) {
$this->context->stdArrays[$argument->name] = $info;
@ -825,14 +826,18 @@ trait StdContainerTrait
/** Return the one-array value-initializer overload, if this call uses it. */
protected function getStdValueInitializer(Expr\StaticCall $expr): ?Expr\Array_
{
if (count($expr->args) !== 1 || !$expr->args[0]->value instanceof Expr\Array_) {
if (count($expr->args) !== 1) {
return null;
}
$argument = $expr->args[0];
$value = $argument->value;
if (!$value instanceof Expr\Array_) {
return null;
}
if ($argument->name !== null || $argument->unpack || $argument->byRef) {
$this->fatalError($argument, 'Std container value initialization requires one positional array argument');
}
return $argument->value;
return $value;
}
/** @return array{type: string, class: ?string} */
@ -893,8 +898,8 @@ trait StdContainerTrait
$valueInfo = null;
$keyType = null;
foreach ($array->items as $item) {
if ($item === null || $item->unpack || $item->byRef) {
$this->fatalError($item ?? $array, $owner . ' initializer does not support holes, unpacking, or references');
if ($item->unpack || $item->byRef) {
$this->fatalError($item, $owner . ' initializer does not support unpacking or references');
}
if ($keyMode === 'positional' && $item->key !== null) {
$this->fatalError($item->key, $owner . ' initializer requires positional array elements');
@ -952,8 +957,8 @@ trait StdContainerTrait
$entries = [];
$nested = null;
foreach ($array->items as $index => $item) {
if ($item === null || $item->unpack || $item->byRef || $item->key !== null) {
$this->fatalError($item ?? $array, $owner . ' initializer must be a positional array without holes, unpacking, or references');
if ($item->unpack || $item->byRef || $item->key !== null) {
$this->fatalError($item, $owner . ' initializer must be a positional array without unpacking or references');
}
$isNested = $item->value instanceof Expr\Array_;

@ -182,6 +182,7 @@ trait TypedArrayTrait
Type::ARRAY => 'Array',
Type::OBJECT => 'Object',
Type::VAR => 'Any',
default => throw new \LogicException('Unknown typed array value type: ' . $definition['type']),
};
$valueClass = $definition['class'] !== null && $definition['class'] !== ''
? $this->getClassEntryPtr($definition['class'])

@ -329,6 +329,30 @@ trait UniversalMethodCall
protected const array MUTATING_HANDLERS = ['direct_method_mutate', 'php_fn_ref'];
/** @var array<string, array<string, true>> */
private const array UNIVERSAL_MUTATING_METHODS = [
Type::ARRAY => [
'sort' => true,
'pop' => true,
'push' => true,
'shift' => true,
'unshift' => true,
'splice' => true,
'walk' => true,
'sortDesc' => true,
'keySort' => true,
'valueSort' => true,
'set' => true,
'del' => true,
'clean' => true,
],
];
protected function isUniversalMethodMutating(string $type, string $method): bool
{
return isset(self::UNIVERSAL_MUTATING_METHODS[$type][$method]);
}
protected function detectUniversalMethodReturnType(string $type, string $method): ?string
{
$builtin = self::UNIVERSAL_METHODS[$type][$method]['return_type'] ?? null;

@ -45,7 +45,7 @@ use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
class Preprocessor extends CompilerBase
abstract class Preprocessor extends CompilerBase
{
/**
* Magic methods forbidden on PHP enums. Enum cases are runtime-managed
@ -986,7 +986,9 @@ class Preprocessor extends CompilerBase
return false;
}
private function hasNoExportAttribute(NodeAbstract $node): bool
private function hasNoExportAttribute(
Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\Stmt\ClassLike $node,
): bool
{
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
@ -1670,8 +1672,9 @@ class Preprocessor extends CompilerBase
}
}
if (!$this->method && $this->canOptimizeMultiReturn($v, $functionDef)) {
$functionDef->multiReturnCount = count($v->stmts[array_key_last($v->stmts)]->expr->items);
$multiReturn = !$this->method ? $this->getOptimizableMultiReturn($v, $functionDef) : null;
if ($multiReturn !== null) {
$functionDef->multiReturnCount = count($multiReturn->items);
// The fixed tuple is an internal ABI detail. PHP and ordinary native
// callers continue to observe an array return value.
$functionDef->returnType = Type::ARRAY;
@ -1721,28 +1724,31 @@ class Preprocessor extends CompilerBase
return $functionDef;
}
private function canOptimizeMultiReturn(Node\Stmt\Function_|Node\Stmt\ClassMethod $function, FunctionDef $functionDef): bool
private function getOptimizableMultiReturn(
Node\Stmt\Function_|Node\Stmt\ClassMethod $function,
FunctionDef $functionDef,
): ?Node\Expr\Array_
{
if ($functionDef->stub || $functionDef->generator || $functionDef->returnsByRef
|| ($functionDef->returnType !== Type::ARRAY && !$functionDef->returnTypeUndeclared)
|| !$function->stmts) {
return false;
return null;
}
$return = $function->stmts[array_key_last($function->stmts)] ?? null;
if (!$return instanceof Node\Stmt\Return_ || !$return->expr instanceof Node\Expr\Array_
|| count($return->expr->items) < 2) {
return false;
return null;
}
$returns = (new NodeFinder())->findInstanceOf($function->stmts, Node\Stmt\Return_::class);
if (count($returns) !== 1) {
return false;
return null;
}
foreach ($return->expr->items as $item) {
if ($item === null || $item->key !== null || $item->unpack || $item->byRef) {
return false;
if ($item->key !== null || $item->unpack || $item->byRef) {
return null;
}
$value = $item->value;
if (($value instanceof Node\Expr\Variable && is_string($value->name))
@ -1750,9 +1756,9 @@ class Preprocessor extends CompilerBase
|| $value instanceof Node\Expr\ConstFetch) {
continue;
}
return false;
return null;
}
return true;
return $return->expr;
}
protected function prepareFunction(Node\Stmt\ClassMethod|Node\Stmt\Function_ $v): void
@ -1975,22 +1981,21 @@ class Preprocessor extends CompilerBase
$code = '';
foreach ($class->stmts as $v) {
$type = $v->getType();
switch ($type) {
case 'Stmt_ClassConst':
break;
case 'Stmt_Property':
if ($v instanceof Node\Stmt\ClassConst || $v instanceof Node\Stmt\Nop) {
continue;
}
if ($v instanceof Node\Stmt\Property) {
if ($this->classDef->enum) {
$this->fatalError($v, "Enum {$fullClassName} cannot include properties");
}
$this->parseClassPropertyDef($v);
break;
case 'Stmt_TraitUse':
continue;
}
if ($v instanceof Node\Stmt\TraitUse) {
$this->prepareTraitUse($v);
break;
case 'Stmt_Nop':
break;
case 'Stmt_EnumCase':
continue;
}
if ($v instanceof Node\Stmt\EnumCase) {
$caseName = $this->parseIdentifier($v->name);
if (array_key_exists($caseName, $this->classDef->enumCases)
|| $this->classDef->hasConstant($caseName)
@ -2009,17 +2014,17 @@ class Preprocessor extends CompilerBase
if ($v->expr !== null) {
$this->classDef->enumCaseExpressions[$caseName] = $v->expr;
}
break;
case 'Stmt_ClassMethod':
continue;
}
if ($v instanceof Node\Stmt\ClassMethod) {
$this->prepareClassMethod($v, $class);
break;
case 'Stmt_Expression':
continue;
}
if ($v instanceof Node\Stmt\Expression) {
$this->foundStrayCode($v);
break;
default:
$this->unsupportedSyntax($v);
break;
continue;
}
$this->unsupportedSyntax($v);
}
// Trait members are later injected into the consuming class for stub
@ -2593,27 +2598,33 @@ class Preprocessor extends CompilerBase
}
$scopeClass ??= $this->getFullClassName();
switch ($node->getType()) {
case 'Scalar_Int':
if ($node instanceof Node\Scalar\Int_) {
return 'int';
case 'Scalar_Float':
}
if ($node instanceof Node\Scalar\Float_) {
return 'float';
case 'Scalar_String':
case 'Scalar_InterpolatedString':
case 'Expr_BinaryOp_Concat':
}
if ($node instanceof Node\Scalar\String_
|| $node instanceof Node\Scalar\InterpolatedString
|| $node instanceof Node\Expr\BinaryOp\Concat
) {
return 'string';
case 'Expr_Array':
}
if ($node instanceof Node\Expr\Array_) {
return 'array';
case 'Expr_UnaryMinus':
}
if ($node instanceof Node\Expr\UnaryMinus) {
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch':
}
if ($node instanceof Node\Expr\ConstFetch) {
return match (strtolower($node->name->toString())) {
'true' => 'true',
'false' => 'false',
'null' => 'null',
default => null,
};
case 'Expr_ClassConstFetch':
}
if ($node instanceof Node\Expr\ClassConstFetch) {
if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) {
return null;
}
@ -2642,14 +2653,18 @@ class Preprocessor extends CompilerBase
return $this->detectDefaultValueType(
$targetDef->getConstant($constName)->valueExpr,
$targetClass,
$depth + 1
$depth + 1,
);
default:
}
if (!$node instanceof Node\Expr) {
return null;
}
try {
$value = (new ConstExprEvaluator(
static function (Node\Expr $expr): never {
throw new \RuntimeException('Unresolved constant expression');
}
},
))->evaluateDirectly($node);
} catch (\Throwable) {
return null;
@ -2665,7 +2680,6 @@ class Preprocessor extends CompilerBase
default => null,
};
}
}
private function propertyTypeAcceptsEnumCase(NodeAbstract $typeNode, string $enumClass): bool
{

@ -8,6 +8,7 @@
namespace TypePhp\Resolver;
use TypePhp\Type;
use TypePhp\Entity\GlobalConstantDef;
use PhpParser\Node;
@ -34,7 +35,7 @@ trait DeclarationSymbolTrait
}
}
protected function finalizeGlobalConstantValue(\stdClass $constant, Node\Expr $expression): void
protected function finalizeGlobalConstantValue(GlobalConstantDef $constant, Node\Expr $expression): void
{
$this->resetFunction();
$constant->value = $this->parseIdentifier($expression);
@ -49,18 +50,17 @@ trait DeclarationSymbolTrait
protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void
{
$constInfo = new \stdClass();
$constInfo->value = $value;
$constInfo->valueExpr = $valueExpr;
$constInfo->type = $this->compilerPhase === self::PHASE_CONVERT
$constInfo = new GlobalConstantDef(
name: $name,
value: $value,
valueExpr: $valueExpr,
namespace: $this->namespace,
sourceFile: $this->file,
type: $this->compilerPhase === self::PHASE_CONVERT
? $this->detectStrValueType($value)
: Type::VAR;
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT;
$constInfo->namespace = $this->namespace;
$constInfo->name = $name;
$constInfo->sourceFile = $this->file;
$constInfo->initializationCode = '';
$constInfo->afterInitializationCode = '';
: Type::VAR,
codegenFinalized: $this->compilerPhase === self::PHASE_CONVERT,
);
$this->constants[$this->escapeConstVar($name)] = $constInfo;
$this->symbolDeclInFile[$this->getConstantDependencySymbol($name)] = $this->file;
}

@ -76,26 +76,41 @@ trait NameResolutionTrait
* gen_stub.php's SimpleType::fromNode() relies on isFullyQualified() to decide whether to re-resolve;
* if the name is not upgraded to FullyQualified, the current namespace prefix is wrongly appended once the context is lost.
*/
protected function upgradeToFullyQualifiedName(?NodeAbstract $type): ?NodeAbstract
protected function upgradeToFullyQualifiedName(
Node\ComplexType|Node\Identifier|Node\Name|null $type,
): Node\ComplexType|Node\Identifier|Node\Name|null
{
if ($type === null) {
return null;
}
if ($type instanceof Node\NullableType) {
return new Node\NullableType($this->upgradeToFullyQualifiedName($type->type));
return new Node\NullableType($this->upgradeDeclaredTypeName($type->type));
}
if ($type instanceof Node\UnionType) {
foreach ($type->types as $i => $subType) {
$type->types[$i] = $this->upgradeToFullyQualifiedName($subType);
$type->types[$i] = $subType instanceof Node\IntersectionType
? $this->upgradeIntersectionType($subType)
: $this->upgradeDeclaredTypeName($subType);
}
return $type;
}
if ($type instanceof Node\IntersectionType) {
return $this->upgradeIntersectionType($type);
}
return $this->upgradeDeclaredTypeName($type);
}
private function upgradeIntersectionType(Node\IntersectionType $type): Node\IntersectionType
{
foreach ($type->types as $i => $subType) {
$type->types[$i] = $this->upgradeToFullyQualifiedName($subType);
$type->types[$i] = $this->upgradeDeclaredTypeName($subType);
}
return $type;
}
private function upgradeDeclaredTypeName(
Node\Identifier|Node\Name $type,
): Node\Identifier|Node\Name {
if ($type instanceof Node\Name\FullyQualified) {
return $type;
}

@ -619,7 +619,7 @@ final class TestCoverageAnalyzer
}
$strings = [];
foreach ($node->value->items as $item) {
if ($item?->value instanceof Node\Scalar\String_) {
if ($item->value instanceof Node\Scalar\String_) {
$strings[] = $item->value->value;
}
}
@ -640,12 +640,12 @@ final class TestCoverageAnalyzer
continue;
}
foreach ($statement->expr->items as $outerItem) {
if (!$outerItem?->value instanceof Expr\Array_) {
if (!$outerItem->value instanceof Expr\Array_) {
continue;
}
$strings = [];
foreach ($outerItem->value->items as $item) {
if ($item?->value instanceof Node\Scalar\String_) {
if ($item->value instanceof Node\Scalar\String_) {
$strings[] = $item->value->value;
}
}

@ -38,7 +38,7 @@ final class ClassFieldSelection
$fields = [];
foreach ($argument->value->items as $item) {
if ($item === null || $item->unpack || $item->key !== null
if ($item->unpack || $item->key !== null
|| !$item->value instanceof Node\Scalar\String_) {
throw new SyntaxError($name . ' $fields must be a list of property-name strings');
}

@ -125,6 +125,9 @@ final class CompileTimeAttribute
public static function consume(Node $node, string $name): bool
{
if (!property_exists($node, 'attrGroups')) {
return false;
}
$found = false;
foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {
@ -146,6 +149,9 @@ final class CompileTimeAttribute
public static function remove(Node $node, string $name): bool
{
if (!property_exists($node, 'attrGroups')) {
return false;
}
$found = false;
foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {

@ -178,7 +178,7 @@ final class ConstantExpressionValidator
if ($expression instanceof Expr\Array_) {
foreach ($expression->items as $item) {
if ($item === null || $item->byRef) {
if ($item->byRef) {
$this->invalidOperation();
}
if ($item->key !== null) {

@ -71,7 +71,11 @@ final class GetterLowering
return self::createGetter($param->var->name, $param->type, $param);
}
private static function createGetter(string $property, ?Node $type, Node $target): Stmt\ClassMethod
private static function createGetter(
string $property,
Node\ComplexType|Node\Identifier|Node\Name|null $type,
Node $target,
): Stmt\ClassMethod
{
$method = new Stmt\ClassMethod('get' . ucfirst($property), [
'flags' => Modifiers::PUBLIC,

@ -47,6 +47,7 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract
Node\Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once',
Node\Expr\Include_::TYPE_REQUIRE => 'require',
Node\Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once',
default => ($this->fatal)($node, 'Invalid include expression type'),
};
($this->fatal)($node, "`{$keyword}` is not supported in nano mode");
}

@ -97,7 +97,11 @@ final class PropertyMethodLowering
}
}
private static function createSetter(string $property, ?Node $type, Node $target): Stmt\ClassMethod
private static function createSetter(
string $property,
Node\ComplexType|Node\Identifier|Node\Name|null $type,
Node $target,
): Stmt\ClassMethod
{
$method = new Stmt\ClassMethod('set' . ucfirst($property), [
'flags' => Modifiers::PUBLIC,
@ -112,7 +116,11 @@ final class PropertyMethodLowering
return $method;
}
private static function createWith(string $property, ?Node $type, Node $target): Stmt\ClassMethod
private static function createWith(
string $property,
Node\ComplexType|Node\Identifier|Node\Name|null $type,
Node $target,
): Stmt\ClassMethod
{
$method = new Stmt\ClassMethod('with' . ucfirst($property), [
'flags' => Modifiers::PUBLIC,

@ -151,7 +151,7 @@ final class ValidateLowering
return null;
}
foreach ($options->items as $item) {
if ($item?->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
if ($item->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
return self::resolveFlags($item->value);
}
}
@ -202,10 +202,10 @@ final class ValidateLowering
}
$flagsItem = null;
foreach ($options->items as $item) {
if ($item?->unpack) {
if ($item->unpack) {
throw new SyntaxError('Validate $options does not support array unpacking');
}
if ($item !== null && $item->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
if ($item->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
$flagsItem = $item;
}
}

@ -449,7 +449,7 @@ class Translator extends Preprocessor
['--march <arch>', 'Target CPU instruction set (for example native or armv8-a)'],
['--target-platform <triple>', 'Cross-compilation target triple'],
['--wasm[=browser|component]', 'Build WASI component (default) or browser output'],
['--nano', 'Use the Nano policy and php-nano runtime outside Windows'],
['--nano', 'Build a Nano application (Windows uses the PHP/PHPX DLL backend)'],
['--full-static', 'Link fully statically against the bundled SDK'],
['--lto', 'Enable Link Time Optimization (-flto)'],
['--no-literal-strings', 'Disable literal string optimization'],
@ -493,12 +493,12 @@ class Translator extends Preprocessor
$this->downloadProxy = $proxy;
}
// The Nano syntax policy is platform-independent. On non-Windows hosts
// only the runtime source and link inputs change; argument parsing,
// translation, compilation scheduling and diagnostics remain shared.
// The Nano syntax policy is platform-independent. Windows produces a
// native application through the PHP/PHPX DLL backend; other targets
// compose the php-nano runtime sources into the artifact.
if ($this->climate->arguments->defined('nano')) {
$this->nanoPolicyMode = true;
if (NanoBuildBackend::composesRuntimeSources(PHP_OS_FAMILY)) {
if (NanoBuildBackend::composesRuntimeSources($this->getPlatform()->getName())) {
$this->nanoMode = true;
$this->noLiteralStrings = true;
}
@ -652,15 +652,18 @@ class Translator extends Preprocessor
$this->linkPaths = $this->parseRepeatableArgv(['-L', '--link-path']);
}
if ($this->isNanoMode()) {
if ($this->isNanoPolicyMode()) {
if ($this->isBuildModeExt()) {
$this->error('--nano source composition does not support extension mode (-m ext)');
$this->error('--nano does not support extension mode (-m ext)');
}
if ($this->cxxStd !== 'c++17') {
$this->error('--nano requires the C++17 language standard');
}
if ($this->fullStatic) {
$this->error('--nano already composes its runtime sources; --full-static is not applicable');
$message = $this->isNanoMode()
? '--nano already composes its runtime sources; --full-static is not applicable'
: '--nano on Windows uses the PHP/PHPX DLL backend; --full-static is not supported';
$this->error($message);
}
// Nano applications may consume target-owned static libraries.
// The final executable dependency audit remains the authority on
@ -763,7 +766,7 @@ class Translator extends Preprocessor
return;
}
$this->climate->warning($source . ' requested but clang-format not found, skipping formatting');
$this->climate->out($source . ' requested but clang-format not found, skipping formatting');
}
protected function formatCppCode(string $file): void
@ -889,7 +892,7 @@ class Translator extends Preprocessor
);
} catch (\Exception $e) {
// Fall back to the legacy logic if initialization fails
$this->climate->warning(
$this->climate->out(
"Failed to initialize new architecture: {$e->getMessage()}. Using legacy mode."
);
$this->platform = null;
@ -1735,8 +1738,8 @@ CODE;
$code .= $this->genCompiledGeneratorFingerprintRegistration();
$code .= '// register constants' . PHP_EOL;
foreach ($this->constants as $name => $const) {
$initializationCode = $const->initializationCode ?? '';
$afterInitializationCode = $const->afterInitializationCode ?? '';
$initializationCode = $const->initializationCode;
$afterInitializationCode = $const->afterInitializationCode;
$scopedInitialization = $initializationCode !== '' || $afterInitializationCode !== '';
if ($scopedInitialization) {
// Each constant has its own temporary namespace. Declaration
@ -2592,7 +2595,7 @@ CODE;
return $this->compileWithProcessPool($sourceFiles, $this->maxJob);
}
$this->climate->warning(
$this->climate->out(
'proc_open/proc_get_status unavailable, using sequential compilation',
);
return $this->compileSourceFile($sourceFiles);
@ -2677,7 +2680,7 @@ CODE;
// PCH is an optimization. A compiler-specific failure must not make
// an otherwise valid TypePHP project unbuildable.
$this->precompiledHeader = null;
$this->climate->warning('[pch] disabled: ' . $e->getMessage());
$this->climate->out('[pch] disabled: ' . $e->getMessage());
}
}

Loading…
Cancel
Save