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" 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 - name: Package tested Windows compiler
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
shell: pwsh shell: pwsh

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

@ -287,9 +287,10 @@ that target are compile-time errors.
On every platform, `--nano` rejects the VM entry paths `eval`, `include`, On every platform, `--nano` rejects the VM entry paths `eval`, `include`,
`include_once`, `require`, and `require_once`, as well as anonymous classes. `include_once`, `require`, and `require_once`, as well as anonymous classes.
Windows uses a different build backend even when `--nano` is specified: it keeps Windows supports Nano native applications in `mode: bin` and `mode: lib` through
the existing host compile/link pipeline and connects to `php.dll` and `phpx.dll` a different build backend: it keeps the existing host compile/link pipeline and
through their import libraries. It does not load the `swoole/php-nano` or 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`. `swoole/phpx` source manifests, nor append their C/C++ files to project `sources`.
External-command APIs and backtick syntax are still rejected. Those command External-command APIs and backtick syntax are still rejected. Those command
functions are also removed from the Zend function table at request startup, so 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->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT);
$this->expectException(TestError::class); $this->expectException(TestError::class);
$this->expectExceptionMessage( $this->expectExceptionMessage('--nano does not support extension mode (-m ext)');
'--nano source composition 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'); $this->invokeMethod('applyCommandLineArguments');
} }

@ -2,22 +2,22 @@
namespace TypePhp\Build; 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 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'; public const WINDOWS_DLL = 'windows-dll';
/** Composer package manifests whose C/C++ sources are compiled into the program. */ /** Composer package manifests whose C/C++ sources are compiled into the program. */
public const COMPOSER_SOURCES = 'composer-sources'; 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); $this->climate->green('Resource compiled: ' . $resFile);
} else { } 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()); $progress = $this->startOpcodeProgress('Opcodes', $batch->pendingCount());
$blobs = $generator->compile( $blobs = $generator->compile(
$batch, $batch,
fn (string $file) => $this->climate->warning( fn (string $file) => $this->climate->out(
'Skipping non-executable embedded PHP file: ' . $file, 'Skipping non-executable embedded PHP file: ' . $file,
), ),
fn (int $completed, int $total, string $file) => $this->updateOpcodeProgress( 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'); $this->error('`sapi` containing cli requires an `entry` PHP file');
} }
if ($this->sapiEntryConfiguredPath !== null && !$this->hasSapi('cli')) { 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->sapiEntryFile = null;
$this->sapiEntryConfiguredPath = null; $this->sapiEntryConfiguredPath = null;
$this->sapiEntryConfiguredValue = null; $this->sapiEntryConfiguredValue = null;
@ -699,7 +699,7 @@ trait SourcePipelineTrait
} }
$this->error($detail); $this->error($detail);
} }
$this->climate->warning($message['warning']); $this->climate->out($message['warning']);
if (!empty($message['info'])) { if (!empty($message['info'])) {
$this->climate->info($message['info']); $this->climate->info($message['info']);
} }

@ -14,7 +14,6 @@ use PhpParser\Node;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
use PhpParser\Parser; use PhpParser\Parser;
use PhpParser\ParserFactory; use PhpParser\ParserFactory;
@ -39,6 +38,7 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef; use TypePhp\Entity\ClassDef;
use TypePhp\Entity\ConstantDef; use TypePhp\Entity\ConstantDef;
use TypePhp\Entity\FunctionDef; use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\GlobalConstantDef;
use TypePhp\Entity\InterfaceDef; use TypePhp\Entity\InterfaceDef;
use TypePhp\Entity\MethodDef; use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef; use TypePhp\Entity\PropertyDef;
@ -103,8 +103,36 @@ use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
use TypePhp\TypeSystem\CompoundTypeDeclarationValidationTrait; use TypePhp\TypeSystem\CompoundTypeDeclarationValidationTrait;
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait; 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 CompositeTypeCheckerTrait;
use CompoundTypeDeclarationValidationTrait; use CompoundTypeDeclarationValidationTrait;
use CompilerDiagnosticTrait; use CompilerDiagnosticTrait;
@ -619,7 +647,7 @@ class CompilerBase implements PropertyAccessContext
protected string $interface = ''; protected string $interface = '';
/** /**
* @var array<string, ConstantDef> * @var array<string, GlobalConstantDef>
*/ */
protected array $constants = []; protected array $constants = [];
@ -729,8 +757,7 @@ class CompilerBase implements PropertyAccessContext
protected array $classMethodOverride = []; protected array $classMethodOverride = [];
/** /**
* Stores all class inheritance relationships. Class names must be all lowercase. * Stores compiler symbols collected during preprocessing.
* @var array<string, string>
*/ */
protected SymbolRepository $symbols; 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 public function getLine($node): int
@ -1425,7 +1453,7 @@ class CompilerBase implements PropertyAccessContext
$this->namespace = ''; $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) { if ($this->methodDef !== null && $this->classDef !== null) {
return $this->getNativeName( return $this->getNativeName(
@ -1899,14 +1927,13 @@ class CompilerBase implements PropertyAccessContext
protected function parseScalar(Node\Scalar $expr): string protected function parseScalar(Node\Scalar $expr): string
{ {
$type = $expr->getType(); if ($expr instanceof Node\Scalar\Int_) {
switch ($type) {
case 'Scalar_Int':
if ($this->bigintTypes) { if ($this->bigintTypes) {
return 'php::toBigInt(' . $expr->value . ')'; return 'php::toBigInt(' . $expr->value . ')';
} }
return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix(); return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix();
case 'Scalar_Float': }
if ($expr instanceof Node\Scalar\Float_) {
if ($this->isBigIntLiteral($expr)) { if ($this->isBigIntLiteral($expr)) {
return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')'; return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')';
} }
@ -1916,13 +1943,13 @@ class CompilerBase implements PropertyAccessContext
return 'php::toDecimal(' . $this->getLiteralString($clean) . ')'; return 'php::toDecimal(' . $this->getLiteralString($clean) . ')';
} }
return $this->parseScalarFloat($expr); 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 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() . ')'; $class = 'Stmt_Expression(' . $v->expr->getType() . ')';
} }
@ -2438,7 +2465,7 @@ class CompilerBase implements PropertyAccessContext
*/ */
protected function parseNumericIdentifier(NodeAbstract $expr): string protected function parseNumericIdentifier(NodeAbstract $expr): string
{ {
if ($expr->getType() === 'Scalar_String') { if ($expr instanceof Node\Scalar\String_) {
if ($this->isFloatStr($expr->value)) { if ($this->isFloatStr($expr->value)) {
return (string) floatval($expr->value); return (string) floatval($expr->value);
} }
@ -3161,7 +3188,7 @@ class CompilerBase implements PropertyAccessContext
$type = str_contains($name, '::') ? 'Method' : 'Function'; $type = str_contains($name, '::') ? 'Method' : 'Function';
if ($argc < $funcDef->argCountRequired) { if ($argc < $funcDef->argCountRequired) {
$this->fatalError($expr, $type . ' `' . $name . '()` requires ' . $funcDef->argCountRequired . ' arguments, ' . $argc . ' given'); $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'); $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'); $this->fatalError($expr, 'Method `' . $classDef->getNamespacedName() . '::' . $method . '()` is not accessible');
} }
// A function-call placeholder, not a real function call. // 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; return false;
} }
if ($checkArgs) { 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); return $this->getNativeName($method, $classDef->namespace, $classDef->name);
} }

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

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

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

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

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

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

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

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

@ -329,6 +329,30 @@ trait UniversalMethodCall
protected const array MUTATING_HANDLERS = ['direct_method_mutate', 'php_fn_ref']; 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 protected function detectUniversalMethodReturnType(string $type, string $method): ?string
{ {
$builtin = self::UNIVERSAL_METHODS[$type][$method]['return_type'] ?? null; $builtin = self::UNIVERSAL_METHODS[$type][$method]['return_type'] ?? null;

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

@ -8,6 +8,7 @@
namespace TypePhp\Resolver; namespace TypePhp\Resolver;
use TypePhp\Type; use TypePhp\Type;
use TypePhp\Entity\GlobalConstantDef;
use PhpParser\Node; 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(); $this->resetFunction();
$constant->value = $this->parseIdentifier($expression); $constant->value = $this->parseIdentifier($expression);
@ -49,18 +50,17 @@ trait DeclarationSymbolTrait
protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void
{ {
$constInfo = new \stdClass(); $constInfo = new GlobalConstantDef(
$constInfo->value = $value; name: $name,
$constInfo->valueExpr = $valueExpr; value: $value,
$constInfo->type = $this->compilerPhase === self::PHASE_CONVERT valueExpr: $valueExpr,
namespace: $this->namespace,
sourceFile: $this->file,
type: $this->compilerPhase === self::PHASE_CONVERT
? $this->detectStrValueType($value) ? $this->detectStrValueType($value)
: Type::VAR; : Type::VAR,
$constInfo->codegenFinalized = $this->compilerPhase === self::PHASE_CONVERT; codegenFinalized: $this->compilerPhase === self::PHASE_CONVERT,
$constInfo->namespace = $this->namespace; );
$constInfo->name = $name;
$constInfo->sourceFile = $this->file;
$constInfo->initializationCode = '';
$constInfo->afterInitializationCode = '';
$this->constants[$this->escapeConstVar($name)] = $constInfo; $this->constants[$this->escapeConstVar($name)] = $constInfo;
$this->symbolDeclInFile[$this->getConstantDependencySymbol($name)] = $this->file; $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; * 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. * 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) { if ($type === null) {
return null; return null;
} }
if ($type instanceof Node\NullableType) { 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) { if ($type instanceof Node\UnionType) {
foreach ($type->types as $i => $subType) { 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; return $type;
} }
if ($type instanceof Node\IntersectionType) { 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) { foreach ($type->types as $i => $subType) {
$type->types[$i] = $this->upgradeToFullyQualifiedName($subType); $type->types[$i] = $this->upgradeDeclaredTypeName($subType);
} }
return $type; return $type;
} }
private function upgradeDeclaredTypeName(
Node\Identifier|Node\Name $type,
): Node\Identifier|Node\Name {
if ($type instanceof Node\Name\FullyQualified) { if ($type instanceof Node\Name\FullyQualified) {
return $type; return $type;
} }

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

@ -38,7 +38,7 @@ final class ClassFieldSelection
$fields = []; $fields = [];
foreach ($argument->value->items as $item) { 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_) { || !$item->value instanceof Node\Scalar\String_) {
throw new SyntaxError($name . ' $fields must be a list of property-name strings'); 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 public static function consume(Node $node, string $name): bool
{ {
if (!property_exists($node, 'attrGroups')) {
return false;
}
$found = false; $found = false;
foreach ($node->attrGroups as $groupIndex => $group) { foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) { foreach ($group->attrs as $attributeIndex => $attribute) {
@ -146,6 +149,9 @@ final class CompileTimeAttribute
public static function remove(Node $node, string $name): bool public static function remove(Node $node, string $name): bool
{ {
if (!property_exists($node, 'attrGroups')) {
return false;
}
$found = false; $found = false;
foreach ($node->attrGroups as $groupIndex => $group) { foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) { foreach ($group->attrs as $attributeIndex => $attribute) {

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

@ -71,7 +71,11 @@ final class GetterLowering
return self::createGetter($param->var->name, $param->type, $param); 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), [ $method = new Stmt\ClassMethod('get' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,

@ -47,6 +47,7 @@ final class NanoSyntaxValidationVisitor extends NodeVisitorAbstract
Node\Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once', Node\Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once',
Node\Expr\Include_::TYPE_REQUIRE => 'require', Node\Expr\Include_::TYPE_REQUIRE => 'require',
Node\Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once', 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"); ($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), [ $method = new Stmt\ClassMethod('set' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,
@ -112,7 +116,11 @@ final class PropertyMethodLowering
return $method; 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), [ $method = new Stmt\ClassMethod('with' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,

@ -151,7 +151,7 @@ final class ValidateLowering
return null; return null;
} }
foreach ($options->items as $item) { 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); return self::resolveFlags($item->value);
} }
} }
@ -202,10 +202,10 @@ final class ValidateLowering
} }
$flagsItem = null; $flagsItem = null;
foreach ($options->items as $item) { foreach ($options->items as $item) {
if ($item?->unpack) { if ($item->unpack) {
throw new SyntaxError('Validate $options does not support array unpacking'); 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; $flagsItem = $item;
} }
} }

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

Loading…
Cancel
Save