diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 3d48ffe4..40edbd37 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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 diff --git a/README-CN.md b/README-CN.md index b589ec2c..c499caa6 100644 --- a/README-CN.md +++ b/README-CN.md @@ -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 函数表移除这些命令函数,避免变量函数或回调形式绕过编译期检查。 diff --git a/README.md b/README.md index 99d2b0de..a8866bf9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 346d70fa..29ddbb78 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -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'); } diff --git a/src/Build/NanoBuildBackend.php b/src/Build/NanoBuildBackend.php index b9584dea..0376d49b 100644 --- a/src/Build/NanoBuildBackend.php +++ b/src/Build/NanoBuildBackend.php @@ -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; } } diff --git a/src/Build/ResourceCompilationTrait.php b/src/Build/ResourceCompilationTrait.php index 20712fe4..00deeb19 100644 --- a/src/Build/ResourceCompilationTrait.php +++ b/src/Build/ResourceCompilationTrait.php @@ -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'); } } } diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 656891fc..7a59a98d 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -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']); } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index b70827d7..870f04cd 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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 + * @var array */ 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 + * 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,30 +1927,29 @@ class CompilerBase implements PropertyAccessContext protected function parseScalar(Node\Scalar $expr): string { - $type = $expr->getType(); - switch ($type) { - case 'Scalar_Int': - if ($this->bigintTypes) { - return 'php::toBigInt(' . $expr->value . ')'; - } - return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix(); - case 'Scalar_Float': - if ($this->isBigIntLiteral($expr)) { - return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')'; - } - if ($this->isDecimalLiteral($expr) || $this->decimalTypes) { - $rawValue = $expr->getAttribute('rawValue'); - $clean = $rawValue !== null ? $this->stripNumericUnderscores($rawValue) : (string) $expr->value; - 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; + if ($expr instanceof Node\Scalar\Int_) { + if ($this->bigintTypes) { + return 'php::toBigInt(' . $expr->value . ')'; + } + return $expr->value . $this->getPlatform()->getIntegerLiteralSuffix(); } - return ''; + if ($expr instanceof Node\Scalar\Float_) { + if ($this->isBigIntLiteral($expr)) { + return 'php::toBigInt(' . $this->getLiteralString($this->getBigIntLiteralString($expr)) . ')'; + } + if ($this->isDecimalLiteral($expr) || $this->decimalTypes) { + $rawValue = $expr->getAttribute('rawValue'); + $clean = $rawValue !== null ? $this->stripNumericUnderscores($rawValue) : (string) $expr->value; + return 'php::toDecimal(' . $this->getLiteralString($clean) . ')'; + } + return $this->parseScalarFloat($expr); + } + 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); } diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 46b2ee52..c2fdd2ba 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -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) diff --git a/src/Immutable/ImmutableSupportTrait.php b/src/Immutable/ImmutableSupportTrait.php index 46d200cc..3626747e 100644 --- a/src/Immutable/ImmutableSupportTrait.php +++ b/src/Immutable/ImmutableSupportTrait.php @@ -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; } diff --git a/src/Metadata/Constants.php b/src/Metadata/Constants.php index 8a84d55c..c5a6adf8 100644 --- a/src/Metadata/Constants.php +++ b/src/Metadata/Constants.php @@ -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 key is the warning number, value is the description + * @var array key is the warning number, value is the description */ public const array MSVC_SUPPRESSED_WARNINGS = [ '4244' => '类型转换可能丢失数据 (int -> smaller type)', diff --git a/src/NativeClass/NativeGlobalTypeResolver.php b/src/NativeClass/NativeGlobalTypeResolver.php index c5c61011..32e6d85a 100644 --- a/src/NativeClass/NativeGlobalTypeResolver.php +++ b/src/NativeClass/NativeGlobalTypeResolver.php @@ -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 $classes - * @param array $constants + * @param array $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; } } diff --git a/src/Parser/AstNodeType.php b/src/Parser/AstNodeType.php index c3869942..171be67a 100644 --- a/src/Parser/AstNodeType.php +++ b/src/Parser/AstNodeType.php @@ -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) diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 65298b68..0cfd740c 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -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, diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index ebb5d8af..2e49f374 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -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_; diff --git a/src/Parser/TypedArrayTrait.php b/src/Parser/TypedArrayTrait.php index 1ea505cb..a7504099 100644 --- a/src/Parser/TypedArrayTrait.php +++ b/src/Parser/TypedArrayTrait.php @@ -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']) diff --git a/src/Parser/UniversalMethodCall.php b/src/Parser/UniversalMethodCall.php index d620dfc1..96343c21 100644 --- a/src/Parser/UniversalMethodCall.php +++ b/src/Parser/UniversalMethodCall.php @@ -329,6 +329,30 @@ trait UniversalMethodCall protected const array MUTATING_HANDLERS = ['direct_method_mutate', 'php_fn_ref']; + /** @var array> */ + 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; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 37a04693..c35f5a7e 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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,51 +1981,50 @@ class Preprocessor extends CompilerBase $code = ''; foreach ($class->stmts as $v) { - $type = $v->getType(); - switch ($type) { - case 'Stmt_ClassConst': - break; - case 'Stmt_Property': - if ($this->classDef->enum) { - $this->fatalError($v, "Enum {$fullClassName} cannot include properties"); - } - $this->parseClassPropertyDef($v); - break; - case 'Stmt_TraitUse': - $this->prepareTraitUse($v); - break; - case 'Stmt_Nop': - break; - case 'Stmt_EnumCase': - $caseName = $this->parseIdentifier($v->name); - if (array_key_exists($caseName, $this->classDef->enumCases) - || $this->classDef->hasConstant($caseName) - ) { - $enumName = $this->classDef->getNamespacedName(false); - $this->fatalError($v, "Cannot redefine class constant {$enumName}::{$caseName}"); - } - // Keep every backing expression until declaration - // finalization. Literal values also seed enumCases for - // declaration consumers, but code generation only accepts - // values finalized after the complete symbol graph exists. - $this->classDef->enumCases[$caseName] = - $v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_ - ? $v->expr->value - : null; - if ($v->expr !== null) { - $this->classDef->enumCaseExpressions[$caseName] = $v->expr; - } - break; - case 'Stmt_ClassMethod': - $this->prepareClassMethod($v, $class); - break; - case 'Stmt_Expression': - $this->foundStrayCode($v); - break; - default: - $this->unsupportedSyntax($v); - break; + 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); + continue; + } + if ($v instanceof Node\Stmt\TraitUse) { + $this->prepareTraitUse($v); + continue; + } + if ($v instanceof Node\Stmt\EnumCase) { + $caseName = $this->parseIdentifier($v->name); + if (array_key_exists($caseName, $this->classDef->enumCases) + || $this->classDef->hasConstant($caseName) + ) { + $enumName = $this->classDef->getNamespacedName(false); + $this->fatalError($v, "Cannot redefine class constant {$enumName}::{$caseName}"); + } + // Keep every backing expression until declaration + // finalization. Literal values also seed enumCases for + // declaration consumers, but code generation only accepts + // values finalized after the complete symbol graph exists. + $this->classDef->enumCases[$caseName] = + $v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_ + ? $v->expr->value + : null; + if ($v->expr !== null) { + $this->classDef->enumCaseExpressions[$caseName] = $v->expr; + } + continue; + } + if ($v instanceof Node\Stmt\ClassMethod) { + $this->prepareClassMethod($v, $class); + continue; } + if ($v instanceof Node\Stmt\Expression) { + $this->foundStrayCode($v); + continue; + } + $this->unsupportedSyntax($v); } // Trait members are later injected into the consuming class for stub @@ -2593,78 +2598,87 @@ class Preprocessor extends CompilerBase } $scopeClass ??= $this->getFullClassName(); - switch ($node->getType()) { - case 'Scalar_Int': - return 'int'; - case 'Scalar_Float': - return 'float'; - case 'Scalar_String': - case 'Scalar_InterpolatedString': - case 'Expr_BinaryOp_Concat': + if ($node instanceof Node\Scalar\Int_) { + return 'int'; + } + if ($node instanceof Node\Scalar\Float_) { + return 'float'; + } + if ($node instanceof Node\Scalar\String_ + || $node instanceof Node\Scalar\InterpolatedString + || $node instanceof Node\Expr\BinaryOp\Concat + ) { + return 'string'; + } + if ($node instanceof Node\Expr\Array_) { + return 'array'; + } + if ($node instanceof Node\Expr\UnaryMinus) { + return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1); + } + if ($node instanceof Node\Expr\ConstFetch) { + return match (strtolower($node->name->toString())) { + 'true' => 'true', + 'false' => 'false', + 'null' => 'null', + default => null, + }; + } + if ($node instanceof Node\Expr\ClassConstFetch) { + if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) { + return null; + } + $constName = $node->name->toString(); + if (strcasecmp($constName, 'class') === 0) { return 'string'; - case 'Expr_Array': - return 'array'; - case 'Expr_UnaryMinus': - return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1); - case 'Expr_ConstFetch': - return match (strtolower($node->name->toString())) { - 'true' => 'true', - 'false' => 'false', - 'null' => 'null', - default => null, - }; - case 'Expr_ClassConstFetch': - if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) { - return null; - } - $constName = $node->name->toString(); - if (strcasecmp($constName, 'class') === 0) { - return 'string'; - } - $className = $node->class->toString(); - if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { - $targetClass = $scopeClass; - } elseif (strcasecmp($className, 'parent') === 0) { - $targetClass = $this->getParentClass($scopeClass); - } else { - $targetClass = $this->getNamespacedClassName($className); - } - if ($targetClass === '' || !$this->hasClass($targetClass)) { - return null; - } - $targetDef = $this->getClass($targetClass); - if ($targetDef->enum && array_key_exists($constName, $targetDef->enumCases)) { - return 'enum:' . $targetDef->getNamespacedName(false); - } - if (!$targetDef->hasConstant($constName)) { - return null; - } - return $this->detectDefaultValueType( - $targetDef->getConstant($constName)->valueExpr, - $targetClass, - $depth + 1 - ); - default: - try { - $value = (new ConstExprEvaluator( - static function (Node\Expr $expr): never { - throw new \RuntimeException('Unresolved constant expression'); - } - ))->evaluateDirectly($node); - } catch (\Throwable) { - return null; - } - return match (true) { - is_int($value) => 'int', - is_float($value) => 'float', - is_string($value) => 'string', - $value === true => 'true', - $value === false => 'false', - is_array($value) => 'array', - $value === null => 'null', - default => null, - }; + } + $className = $node->class->toString(); + if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { + $targetClass = $scopeClass; + } elseif (strcasecmp($className, 'parent') === 0) { + $targetClass = $this->getParentClass($scopeClass); + } else { + $targetClass = $this->getNamespacedClassName($className); + } + if ($targetClass === '' || !$this->hasClass($targetClass)) { + return null; + } + $targetDef = $this->getClass($targetClass); + if ($targetDef->enum && array_key_exists($constName, $targetDef->enumCases)) { + return 'enum:' . $targetDef->getNamespacedName(false); + } + if (!$targetDef->hasConstant($constName)) { + return null; + } + return $this->detectDefaultValueType( + $targetDef->getConstant($constName)->valueExpr, + $targetClass, + $depth + 1, + ); + } + + 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; + } + return match (true) { + is_int($value) => 'int', + is_float($value) => 'float', + is_string($value) => 'string', + $value === true => 'true', + $value === false => 'false', + is_array($value) => 'array', + $value === null => 'null', + default => null, + }; } private function propertyTypeAcceptsEnumCase(NodeAbstract $typeNode, string $enumClass): bool diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index 5d8fa52d..c9a5861d 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -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 - ? $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 = ''; + $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, + codegenFinalized: $this->compilerPhase === self::PHASE_CONVERT, + ); $this->constants[$this->escapeConstVar($name)] = $constInfo; $this->symbolDeclInFile[$this->getConstantDependencySymbol($name)] = $this->file; } diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index ac009e6f..f31254de 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -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) { - foreach ($type->types as $i => $subType) { - $type->types[$i] = $this->upgradeToFullyQualifiedName($subType); - } - return $type; + 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->upgradeDeclaredTypeName($subType); } + return $type; + } + + private function upgradeDeclaredTypeName( + Node\Identifier|Node\Name $type, + ): Node\Identifier|Node\Name { if ($type instanceof Node\Name\FullyQualified) { return $type; } diff --git a/src/Testing/TestCoverageAnalyzer.php b/src/Testing/TestCoverageAnalyzer.php index 3757611d..09e819b8 100644 --- a/src/Testing/TestCoverageAnalyzer.php +++ b/src/Testing/TestCoverageAnalyzer.php @@ -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; } } diff --git a/src/Transform/ClassFieldSelection.php b/src/Transform/ClassFieldSelection.php index e5084638..af907584 100644 --- a/src/Transform/ClassFieldSelection.php +++ b/src/Transform/ClassFieldSelection.php @@ -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'); } diff --git a/src/Transform/CompileTimeAttribute.php b/src/Transform/CompileTimeAttribute.php index 68a7f88c..74239ad9 100644 --- a/src/Transform/CompileTimeAttribute.php +++ b/src/Transform/CompileTimeAttribute.php @@ -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) { diff --git a/src/Transform/ConstantExpressionValidator.php b/src/Transform/ConstantExpressionValidator.php index 357a9466..c64393a5 100644 --- a/src/Transform/ConstantExpressionValidator.php +++ b/src/Transform/ConstantExpressionValidator.php @@ -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) { diff --git a/src/Transform/GetterLowering.php b/src/Transform/GetterLowering.php index 2f690434..2ddaa2a9 100644 --- a/src/Transform/GetterLowering.php +++ b/src/Transform/GetterLowering.php @@ -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, diff --git a/src/Transform/NanoSyntaxValidationVisitor.php b/src/Transform/NanoSyntaxValidationVisitor.php index 0b0824df..5435e5cb 100644 --- a/src/Transform/NanoSyntaxValidationVisitor.php +++ b/src/Transform/NanoSyntaxValidationVisitor.php @@ -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"); } diff --git a/src/Transform/PropertyMethodLowering.php b/src/Transform/PropertyMethodLowering.php index 2efeefb6..df1a705f 100644 --- a/src/Transform/PropertyMethodLowering.php +++ b/src/Transform/PropertyMethodLowering.php @@ -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, diff --git a/src/Transform/ValidateLowering.php b/src/Transform/ValidateLowering.php index 10827912..ad58f2ea 100644 --- a/src/Transform/ValidateLowering.php +++ b/src/Transform/ValidateLowering.php @@ -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; } } diff --git a/src/Translator.php b/src/Translator.php index b2058a8f..043fd355 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -449,7 +449,7 @@ class Translator extends Preprocessor ['--march ', 'Target CPU instruction set (for example native or armv8-a)'], ['--target-platform ', '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()); } }