diff --git a/.github/workflows/turbo-ext.yml b/.github/workflows/turbo-ext.yml new file mode 100644 index 00000000000..cc350f7aafc --- /dev/null +++ b/.github/workflows/turbo-ext.yml @@ -0,0 +1,235 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Turbo Extension" + +permissions: + contents: read + +on: + pull_request: + push: + branches: + - "2.2.x" + +concurrency: + group: turbo-ext-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches + cancel-in-progress: true + +env: + # Pin PHP-CPP to the commit the extension was developed against. + PHP_CPP_COMMIT: "382a5f0386c8c46650e670366418e6ef55e71830" + # The php-parser version whose grammar tables and semantic actions the + # native parser engine (turbo-ext/src/parser/) was ported against. + SUPPORTED_PHP_PARSER_VERSION: "v5.8.0" + +jobs: + version: + name: "Extension Version Check" + runs-on: "ubuntu-latest" + timeout-minutes: 5 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 # git log over turbo-ext/src needs full history + + - name: "Install PHP" + uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 + with: + coverage: "none" + php-version: "8.5" + + - name: "Check the shadowed PHP and C++ implementations are in sync (method parity)" + run: "php turbo-ext/bin/side-by-side.php --check" + + - name: "Check the vendored php-parser matches the version the native parser engine was ported against" + run: | + INSTALLED="$(jq -r '.packages[] | select(.name == "nikic/php-parser") | .version' composer.lock)" + echo "composer.lock: $INSTALLED, native engine ported against: $SUPPORTED_PHP_PARSER_VERSION" + if [ "$INSTALLED" != "$SUPPORTED_PHP_PARSER_VERSION" ]; then + echo "::error::nikic/php-parser was updated to $INSTALLED but turbo-ext/src/parser/ was ported against $SUPPORTED_PHP_PARSER_VERSION." + echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin." + exit 1 + fi + + - name: "Check TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION against the last commit touching turbo-ext/src or a shadowed PHP class" + run: | + # A change to either side of a shadowed pair must bump the version: + # watch turbo-ext/src plus the repo-local PHP files from the manifest + # (vendored entries are governed by composer.lock instead). + mapfile -t MAPPED_PHP < <(jq -r '.[] | select(.vendored != true) | .php' turbo-ext/shadowed-classes.json) + EXPECTED_SHA="$(git log -1 --format=%H -- turbo-ext/src "${MAPPED_PHP[@]}" | cut -c1-7)" + ENABLER="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" + echo "last commit touching turbo-ext/src or a shadowed PHP class: $EXPECTED_SHA" + echo "TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION: $ENABLER" + if [ "$ENABLER" != "$EXPECTED_SHA" ]; then + echo "::error::TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION must be the short SHA of the last commit touching turbo-ext/src/ or a PHP class listed in turbo-ext/shadowed-classes.json." + echo "::error::After changing either side, verify the implementations still match and add a follow-up commit setting the constant to that commit's short SHA (the binary bakes its version from git at build time)." + exit 1 + fi + + compile: + name: "Compile Extension" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + operating-system: ["ubuntu-latest", "macos-latest"] + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # The Makefile bakes the extension version via git log over the + # watched paths; a shallow clone would resolve every build to the + # checked-out commit itself — on pull requests the synthetic merge + # commit, which matches nothing. + fetch-depth: 0 + + - name: "Install PHP" + uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 + with: + coverage: "none" + php-version: "8.5" + + - name: "Cache PHP-CPP" + id: "cache-php-cpp" + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: "turbo-ext/PHP-CPP" + key: "php-cpp-${{ env.PHP_CPP_COMMIT }}-php-8.5-${{ matrix.operating-system }}" + + - name: "Clone and build PHP-CPP" + if: steps.cache-php-cpp.outputs.cache-hit != 'true' + working-directory: "turbo-ext" + run: | + git clone https://github.com/CopernicaMarketingSoftware/PHP-CPP.git PHP-CPP + git -C PHP-CPP checkout "$PHP_CPP_COMMIT" + ln -sfn include PHP-CPP/phpcpp + # On LP64 Darwin int64_t is long long, so returning long from + # count() makes the Php::Value construction ambiguous; the cast is a + # no-op on Linux, so the patch applies everywhere. + git -C PHP-CPP apply ../patches/php-cpp-base-count-int64.patch + make -C PHP-CPP -j"$(getconf _NPROCESSORS_ONLN)" + + - name: "Compile phpstan_turbo with strict warnings" + working-directory: "turbo-ext" + # Exemptions, each caused by third-party macro expansions, not our code: + # -Wno-assume: zend's parameter-parsing macros expand __builtin_assume + # with (potential) side effects + # -Wno-unused-parameter: PHP_METHOD's fixed signature + # (execute_data/return_value are not used by every method) + # -Wno-unicode: zend arginfo macros stringify namespaced class names; + # clang lexes the \N in "PhpParser\NodeVisitor" as a universal + # character name (GCC ignores the unknown -Wno- flag) + run: | + ln -sfn include PHP-CPP/phpcpp + make WARN_FLAGS="-Wall -Wextra -Werror -Wno-assume -Wno-unused-parameter -Wno-unicode" + + - name: "Verify the built extension reports the expected version" + run: | + REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" + EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" + echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" + [ "$REPORTED" = "$EXPECTED" ] + + - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 + + - name: "Smoke test (differential: native vs PHP implementations)" + run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/smoke.php + + - name: "Signature parity (reflect native classes against the PHP twins)" + run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/signature-parity.php + + - name: "Parser actions are generated (regenerate and diff)" + run: | + php turbo-ext/bin/generate-parser-actions.php + git diff --exit-code turbo-ext/src/parser/ + + - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" + run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -d memory_limit=4G turbo-ext/tests/parser-corpus.php + + - name: "Upload extension artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: "phpstan_turbo-${{ matrix.operating-system }}" + path: "turbo-ext/phpstan_turbo.so" + if-no-files-found: "error" + + run: + name: "Run with Extension" + needs: "compile" + runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 + + strategy: + fail-fast: false + matrix: + operating-system: ["ubuntu-latest", "macos-latest"] + script: ["make tests", "make phpstan"] + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: "Install PHP" + uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 + with: + coverage: "none" + php-version: "8.5" + + - name: "Download extension artifact" + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "phpstan_turbo-${{ matrix.operating-system }}" + path: "turbo-ext" + + - name: "Install extension" + run: | + sudo bash -c "echo 'extension=$GITHUB_WORKSPACE/turbo-ext/phpstan_turbo.so' >> $(php -r 'echo php_ini_loaded_file();')" + php -m | grep phpstan_turbo + + - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 + + - name: "Verify the extension is active" + run: | + php -r ' + require "src/Turbo/TurboExtensionEnabler.php"; + PHPStan\Turbo\TurboExtensionEnabler::enableIfLoaded(); + require "vendor/autoload.php"; + if (!(new ReflectionClass("PHPStan\\Analyser\\ScopeOps"))->getParentClass() || !(new ReflectionClass("PHPStan\\Analyser\\ScopeOps"))->getParentClass()->isInternal()) { + fwrite(STDERR, "turbo extension is not shadowing ScopeOps" . PHP_EOL); + exit(1); + } + ' + + - name: "Run" + # The memo's per-worker footprint grows with files-per-worker; macOS + # runners have few cores, so each of their few workers analyses enough + # files to tip the default 450M limit that a many-worker Linux runner + # stays under. + env: + PHPSTAN_MEMORY_LIMIT: ${{ matrix.operating-system == 'macos-latest' && '1G' || '450M' }} + run: ${{ matrix.script }} diff --git a/Makefile b/Makefile index bfece2ac9cc..701b9e1ff16 100644 --- a/Makefile +++ b/Makefile @@ -159,8 +159,10 @@ cs: cs-install cs-fix: cs-install XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf +PHPSTAN_MEMORY_LIMIT ?= 450M + phpstan: - php bin/phpstan clear-result-cache -q && php -d memory_limit=450M bin/phpstan -v + php bin/phpstan clear-result-cache -q && php -d memory_limit=$(PHPSTAN_MEMORY_LIMIT) bin/phpstan -v phpstan-result-cache: php -d memory_limit=450M bin/phpstan diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php index 10e19107ddd..93e85b7a92a 100644 --- a/build/composer-dependency-analyser.php +++ b/build/composer-dependency-analyser.php @@ -23,6 +23,17 @@ 'nette/php-generator', ]; +$unknownClasses = [ + 'JetBrains\PhpStorm\Pure', // not present on composer's classmap + 'PHPStan\ExtensionInstaller\GeneratedConfig', // generated +]; +if (!class_exists('PHPStanTurbo\Runtime')) { + // provided by the optional phpstan_turbo extension (turbo-ext/); the ignore + // must be conditional — on machines with the extension loaded the class + // exists and a static ignore would be reported as unused + $unknownClasses[] = 'PHPStanTurbo\Runtime'; +} + return $config ->addPathToScan(__DIR__ . '/../bin', true) ->ignoreErrorsOnPackages( @@ -35,7 +46,4 @@ ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling ->ignoreErrorsOnPackage('jetbrains/phpstorm-stubs', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]) // there is no direct usage, but we need newer version then required by ondrejmirtes/BetterReflection ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION, ErrorType::SHADOW_DEPENDENCY]) // to be able to test invalid symbols - ->ignoreUnknownClasses([ - 'JetBrains\PhpStorm\Pure', // not present on composer's classmap - 'PHPStan\ExtensionInstaller\GeneratedConfig', // generated - ]); + ->ignoreUnknownClasses($unknownClasses); diff --git a/build/phpstan.neon b/build/phpstan.neon index 55bca7cb8e9..d5d4b52cc29 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -19,6 +19,8 @@ parameters: - ../tests bootstrapFiles: - ../tests/phpstan-bootstrap.php + scanFiles: + - ../turbo-ext/analysis-stub.php cache: nodesByStringCountMax: 128 checkUninitializedProperties: true diff --git a/conf/config.neon b/conf/config.neon index b622c8a6dd1..bddac2e56af 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -2,7 +2,6 @@ includes: - parametersSchema.neon - services.neon - parsers.neon - - turbo.neon parameters: bootstrapFiles: diff --git a/conf/turbo.neon b/conf/turbo.neon deleted file mode 100644 index 27ecd8a300e..00000000000 --- a/conf/turbo.neon +++ /dev/null @@ -1,7 +0,0 @@ -services: - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\SymbolFinderInFiles - arguments: - cleaner: @PHPStan\Reflection\BetterReflection\SourceLocator\PhpFileCleaner - - - class: PHPStan\Reflection\BetterReflection\SourceLocator\PhpFileCleaner diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b5777ca536b..009e36ddf62 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -69,7 +69,7 @@ parameters: - rawMessage: Casting to string something that's already string. identifier: cast.useless - count: 3 + count: 1 path: src/Analyser/MutatingScope.php - @@ -114,6 +114,12 @@ parameters: count: 1 path: src/Analyser/RuleErrorTransformer.php + - + rawMessage: Casting to string something that's already string. + identifier: cast.useless + count: 2 + path: src/Analyser/ScopeOps.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 2d72d50bef9..3a662bcbf73 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -463,7 +463,12 @@ public function processAssignVar( } $truthyType = TypeCombinator::removeFalsey($type); - if ($truthyType !== $type) { + // Value comparison, not identity: remove() happens to hand back the very same + // instance when it removes nothing, but that is not part of its contract — the + // falsey loop below already compares with equals(). The identity check is only + // a fast path (equals() has no such shortcut, and no-op removal is the common + // case here). + if ($truthyType !== $type && !$truthyType->equals($type)) { $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5831d4d0b7b..1f843048b25 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -6,7 +6,6 @@ use PhpParser\Node\Arg; use PhpParser\Node\ComplexType; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Match_; @@ -39,7 +38,6 @@ use PHPStan\Node\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\VirtualNode; -use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\Php\PhpVersionFactory; @@ -107,7 +105,6 @@ use function abs; use function array_filter; use function array_key_exists; -use function array_key_first; use function array_keys; use function array_last; use function array_map; @@ -119,7 +116,6 @@ use function assert; use function count; use function explode; -use function get_class; use function implode; use function in_array; use function is_array; @@ -127,13 +123,11 @@ use function ltrim; use function md5; use function sprintf; -use function str_contains; use function str_starts_with; use function strlen; use function strtolower; use function substr; use function uksort; -use function usort; use const PHP_INT_MAX; use const PHP_INT_MIN; use const PHP_VERSION_ID; @@ -142,12 +136,13 @@ class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter { public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; - private const CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME = 'containsSuperGlobal'; - private const COMPLEX_UNION_TYPE_MEMBER_LIMIT = 8; - /** @var Type[] */ - private array $resolvedTypes = []; + /** + * @internal accessed by ScopeOps (native and PHP implementations) + * @var array + */ + public array $resolvedTypes = []; /** @var array */ private array $truthyScopes = []; @@ -358,7 +353,8 @@ public function rememberConstructorScope(): self ); } - private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool + /** @internal called by ScopeOps */ + public function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool { if (!$this->phpVersion->supportsReadOnlyProperties()) { return false; @@ -716,20 +712,7 @@ public function invalidateExistenceCheckExpressions(array $functionNames, ?strin /** @api */ public function hasVariableType(string $variableName): TrinaryLogic { - if ($this->isGlobalVariable($variableName)) { - return TrinaryLogic::createYes(); - } - - $varExprString = '$' . $variableName; - if (!isset($this->expressionTypes[$varExprString])) { - if ($this->canAnyVariableExist()) { - return TrinaryLogic::createMaybe(); - } - - return TrinaryLogic::createNo(); - } - - return $this->expressionTypes[$varExprString]->getCertainty(); + return ScopeOps::hasVariableType($this, $variableName); } /** @api */ @@ -971,12 +954,12 @@ public function getAnonymousFunctionReturnType(): ?Type /** @api */ public function getType(Expr $node): Type { - $key = $this->getNodeKey($node); - - if (!array_key_exists($key, $this->resolvedTypes)) { - $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); + $type = ScopeOps::getTypeFromCache($this, $node, $key); + if ($type !== null) { + return $type; } - return $this->resolvedTypes[$key]; + + return $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node)); } public function getScopeType(Expr $expr): Type @@ -996,25 +979,56 @@ public function getNodeKey(Expr $node): string return '$' . $node->name; } - $key = $this->exprPrinter->printExpr($node); - $attributes = $node->getAttributes(); - if ( - $node instanceof Node\FunctionLike - && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) - && (($attributes['startFilePos'] ?? null) !== null) - ) { - $key .= '/*' . $attributes['startFilePos']; - foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] as $arg) { - $key .= ':' . $this->exprPrinter->printExpr($arg->value); - } - $key .= '*/'; - } + return ScopeOps::nodeKey($node, $this->exprPrinter); + } - if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { - $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/'; - } + /** @internal */ + public function getExprPrinter(): ExprPrinter + { + return $this->exprPrinter; + } - return $key; + /** + * Creates a copy of this scope with the given expression tables and flags + * replaced, keeping context, function, namespace and everything else. + * + * @internal called by ScopeOps + * @param array $expressionTypes + * @param array $nativeExpressionTypes + * @param array $conditionalExpressions + * @param array $currentlyAssignedExpressions + * @param array $currentlyAllowedUndefinedExpressions + * @param list $inFunctionCallsStack + */ + public function duplicateWith( + array $expressionTypes, + array $nativeExpressionTypes, + array $conditionalExpressions, + array $currentlyAssignedExpressions, + array $currentlyAllowedUndefinedExpressions, + array $inFunctionCallsStack, + bool $inFirstLevelStatement, + bool $afterExtractCall, + ): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $inFirstLevelStatement, + $currentlyAssignedExpressions, + $currentlyAllowedUndefinedExpressions, + $inFunctionCallsStack, + $afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); } public function getClosureScopeCacheKey(): string @@ -1050,13 +1064,9 @@ private function resolveType(string $exprString, Expr $node): Type } } - if ( - !$node instanceof Variable - && !$node instanceof Expr\Closure - && !$node instanceof Expr\ArrowFunction - && $this->hasExpressionType($node)->yes() - ) { - return $this->expressionTypes[$exprString]->getType(); + $expressionType = ScopeOps::expressionTypeByKey($this, $node, $exprString); + if ($expressionType !== null) { + return $expressionType; } $exprHandler = ExprHandlerRegistry::resolve($node, $this->container); @@ -1370,15 +1380,7 @@ public function getTypeFromValue($value): Type /** @api */ public function hasExpressionType(Expr $node): TrinaryLogic { - if ($node instanceof Variable && is_string($node->name)) { - return $this->hasVariableType($node->name); - } - - $exprString = $this->getNodeKey($node); - if (!isset($this->expressionTypes[$exprString])) { - return TrinaryLogic::createNo(); - } - return $this->expressionTypes[$exprString]->getCertainty(); + return ScopeOps::hasExpressionType($this, $node, $this->exprPrinter); } /** @@ -2829,7 +2831,7 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp array_merge($intertwinedPropagatedFrom, [$variableName]), ); } else { - $targetRootVar = $this->getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr()); + $targetRootVar = ScopeOps::getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr()); if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) { continue; } @@ -2978,23 +2980,17 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, $nativeTypes = $scope->nativeExpressionTypes; $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); - $scope = $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), + /** @var static $scope */ + $scope = ScopeOps::scopeWith( + $this, $expressionTypes, $nativeTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, + $this->inFirstLevelStatement, $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); if ($expr instanceof AlwaysRememberedExpr) { @@ -3057,208 +3053,39 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self { - $expressionTypes = $this->expressionTypes; - $nativeExpressionTypes = $this->nativeExpressionTypes; - $invalidated = false; $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); - foreach ($expressionTypes as $exprString => $exprTypeHolder) { - $exprExpr = $exprTypeHolder->getExpr(); - if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) { - continue; - } - - unset($expressionTypes[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; - } - - $newConditionalExpressions = []; - foreach ($this->conditionalExpressions as $conditionalExprString => $holders) { - if (count($holders) === 0) { - continue; - } - $firstExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr(); - if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), $requireMoreCharacters, $invalidatingClass)) { - $invalidated = true; - continue; - } - $filteredHolders = []; - foreach ($holders as $key => $holder) { - $shouldKeep = true; - $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders(); - foreach ($conditionalTypeHolders as $conditionalTypeHolderExprString => $conditionalTypeHolder) { - if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr(), $conditionalTypeHolderExprString, false, $invalidatingClass)) { - $invalidated = true; - $shouldKeep = false; - break; - } - } - if (!$shouldKeep) { - continue; - } - - $filteredHolders[$key] = $holder; - } - if (count($filteredHolders) <= 0) { - continue; - } - - $newConditionalExpressions[$conditionalExprString] = $filteredHolders; - } - - if (!$invalidated) { + $result = ScopeOps::invalidateExpressionEntries( + $this, + $this->exprPrinter, + $exprStringToInvalidate, + $expressionToInvalidate, + $requireMoreCharacters, + $invalidatingClass, + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + ); + if ($result === null) { return $this; } - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $expressionTypes, - $nativeExpressionTypes, - $newConditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, + /** @var static */ + return ScopeOps::scopeWith( + $this, + $result[0], + $result[1], + $result[2], $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, [], + $this->inFirstLevelStatement, $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); } - private function getIntertwinedRefRootVariableName(Expr $expr): ?string - { - if ($expr instanceof Variable && is_string($expr->name)) { - return $expr->name; - } - if ($expr instanceof Expr\ArrayDimFetch) { - return $this->getIntertwinedRefRootVariableName($expr->var); - } - return null; - } - - private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool - { - if ( - $expr instanceof IntertwinedVariableByReferenceWithExpr - && $exprToInvalidate instanceof Variable - && is_string($exprToInvalidate->name) - && ( - $expr->getVariableName() === $exprToInvalidate->name - || $this->getIntertwinedRefRootVariableName($expr->getExpr()) === $exprToInvalidate->name - || $this->getIntertwinedRefRootVariableName($expr->getAssignedExpr()) === $exprToInvalidate->name - ) - ) { - return false; - } - - if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) { - return false; - } - - // Variables will not contain traversable expressions. skip the NodeFinder overhead - if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) { - return $exprStringToInvalidate === $exprString; - } - - // getNodeKey() is the pretty-printed expression, and the standard printer is - // compositional: the key of any sub-expression appears verbatim as a substring of - // the key of the expression containing it. So if the invalidated expression's key - // does not appear anywhere in this expression's key, this expression cannot contain - // it and we can skip the expensive AST traversal below. - // Carve-outs where that invariant does not hold: - // - '$this' is special-cased in the visitor to also match self/static/parent, - // - PHPStan's virtual nodes (printed as '__phpstan…') use non-compositional printers - // (e.g. a wrapped variable is printed by name, not as '$name'), - // - keys carrying a getNodeKey() suffix ('/*…*/') are not plain substrings. - if ( - $exprStringToInvalidate !== '$this' - && !str_contains($exprStringToInvalidate, '__phpstan') - && !str_contains($exprStringToInvalidate, '/*') - && !str_contains($exprString, '__phpstan') - && !str_contains($exprString, $exprStringToInvalidate) - ) { - return false; - } - - if (!$this->containsExpressionToInvalidate($expr, get_class($exprToInvalidate), $exprStringToInvalidate)) { - return false; - } - - if ( - $expr instanceof PropertyFetch - && $requireMoreCharacters - && $this->isReadonlyPropertyFetch($expr, false) - ) { - return false; - } - - if ( - $invalidatingClass !== null - && $requireMoreCharacters - && $this->isPrivatePropertyOfDifferentClass($expr, $invalidatingClass) - ) { - return false; - } - - return true; - } - - /** - * Depth-first pre-order search for the invalidated expression, replacing a - * NodeFinder::findFirst() call - this runs for every (stored expression, - * invalidated expression) pair whose keys pass the substring pre-filter, - * so the traverser/visitor machinery overhead was significant. - * - * @param class-string $expressionToInvalidateClass - */ - private function containsExpressionToInvalidate(Node $node, string $expressionToInvalidateClass, string $exprStringToInvalidate): bool - { - if ( - $exprStringToInvalidate === '$this' - && $node instanceof Name - && ( - in_array($node->toLowerString(), ['self', 'static', 'parent'], true) - || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node))) - ) - ) { - return true; - } - - if ( - $node instanceof $expressionToInvalidateClass - && $this->getNodeKey($node) === $exprStringToInvalidate - ) { - return true; - } - - foreach ($node->getSubNodeNames() as $subNodeName) { - $subNode = $node->$subNodeName; - if ($subNode instanceof Node) { - if ($this->containsExpressionToInvalidate($subNode, $expressionToInvalidateClass, $exprStringToInvalidate)) { - return true; - } - } elseif (is_array($subNode)) { - foreach ($subNode as $subNodeItem) { - if ( - $subNodeItem instanceof Node - && $this->containsExpressionToInvalidate($subNodeItem, $expressionToInvalidateClass, $exprStringToInvalidate) - ) { - return true; - } - } - } - } - - return false; - } - - private function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool + /** @internal called by ScopeOps */ + public function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool { if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof PropertyFetch) { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); @@ -3276,47 +3103,27 @@ private function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self { - $exprStringToInvalidate = null; - $expressionTypes = $this->expressionTypes; - $nativeExpressionTypes = $this->nativeExpressionTypes; - $invalidated = false; - foreach ($expressionTypes as $exprString => $exprTypeHolder) { - $expr = $exprTypeHolder->getExpr(); - if (!$expr instanceof MethodCall) { - continue; - } - - $exprStringToInvalidate ??= $this->getNodeKey($expressionToInvalidate); - if ($this->getNodeKey($expr->var) !== $exprStringToInvalidate) { - continue; - } - - unset($expressionTypes[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; - } - - if (!$invalidated) { + $result = ScopeOps::invalidateMethodsOnExpression( + $this->exprPrinter, + $this->getNodeKey($expressionToInvalidate), + $this->expressionTypes, + $this->nativeExpressionTypes, + ); + if ($result === null) { return $this; } - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $expressionTypes, - $nativeExpressionTypes, + /** @var static */ + return ScopeOps::scopeWith( + $this, + $result[0], + $result[1], $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, [], + $this->inFirstLevelStatement, $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); } @@ -3449,38 +3256,7 @@ public function filterByFalseyValue(Expr $expr): self */ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { - $typeSpecifications = []; - foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { - if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { - continue; - } - $typeSpecifications[] = [ - 'sure' => true, - 'exprString' => (string) $exprString, - 'expr' => $expr, - 'type' => $type, - ]; - } - foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { - if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { - continue; - } - $typeSpecifications[] = [ - 'sure' => false, - 'exprString' => (string) $exprString, - 'expr' => $expr, - 'type' => $type, - ]; - } - - usort($typeSpecifications, static function (array $a, array $b): int { - $length = strlen($a['exprString']) - strlen($b['exprString']); - if ($length !== 0) { - return $length; - } - - return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric - }); + $typeSpecifications = ScopeOps::buildTypeSpecifications($specifiedTypes->getSureTypes(), $specifiedTypes->getSureNotTypes()); $scope = $this; $specifiedExpressions = []; @@ -3516,62 +3292,17 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getScopeType($expr)); } - $conditions = []; - $originallySpecifiedExprStrings = $specifiedExpressions; - $prevSpecifiedCount = -1; - while (count($specifiedExpressions) !== $prevSpecifiedCount) { - $prevSpecifiedCount = count($specifiedExpressions); - foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { - if (array_key_exists($conditionalExprString, $conditions)) { - continue; - } + [$conditions] = ScopeOps::matchConditionalExpressions($scope->conditionalExpressions, $specifiedExpressions); - // Pass 1: Prefer exact matches - foreach ($conditionalExpressions as $conditionalExpression) { - if ( - $conditionalExpression->getTypeHolder()->getCertainty()->no() - && array_key_exists($conditionalExprString, $originallySpecifiedExprStrings) - ) { - continue; - } - foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if ( - !array_key_exists($holderExprString, $specifiedExpressions) - || !$conditionalTypeHolder->equals($specifiedExpressions[$holderExprString]) - ) { - continue 2; - } - } - - $conditions[$conditionalExprString][] = $conditionalExpression; - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); - } - - if (array_key_exists($conditionalExprString, $conditions)) { - continue; - } - - // Pass 2: Supertype match. Only runs when Pass 1 found no exact match for this expression. - foreach ($conditionalExpressions as $conditionalExpression) { - if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) { - continue; - } - foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if ( - !array_key_exists($holderExprString, $specifiedExpressions) - || !$conditionalTypeHolder->getCertainty()->equals($specifiedExpressions[$holderExprString]->getCertainty()) - || !$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedExpressions[$holderExprString]->getType())->yes() - ) { - continue 2; - } - } - - $conditions[$conditionalExprString][] = $conditionalExpression; - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); - } - } - } + return $this->applyFilteredConditions($scope, $conditions, $specifiedTypes); + } + /** + * @param array $conditions + * @return static + */ + private function applyFilteredConditions(self $scope, array $conditions, SpecifiedTypes $specifiedTypes): self + { foreach ($conditions as $conditionalExprString => $expressions) { $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); if ($certainty->no()) { @@ -3595,23 +3326,16 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } /** @var static */ - return $scope->scopeFactory->create( - $scope->context, - $scope->isDeclareStrictTypes(), - $scope->getFunction(), - $scope->getNamespace(), + return ScopeOps::scopeWith( + $scope, $scope->expressionTypes, $scope->nativeExpressionTypes, $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), - $scope->inClosureBindScopeClasses, - $scope->anonymousFunctionReflection, - $scope->inFirstLevelStatement, $scope->currentlyAssignedExpressions, $scope->currentlyAllowedUndefinedExpressions, $scope->inFunctionCallsStack, + $scope->inFirstLevelStatement, $scope->afterExtractCall, - $scope->parentScope, - $scope->nativeTypesPromoted, ); } @@ -3641,23 +3365,18 @@ public function addConditionalExpressions(string $exprString, array $conditional $existing[$holder->getKey()] = $holder; } $conditionalExpressions[$exprString] = $existing; - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), + + /** @var static */ + return ScopeOps::scopeWith( + $this, $this->expressionTypes, $this->nativeExpressionTypes, $conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, + $this->inFirstLevelStatement, $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); } @@ -3671,23 +3390,17 @@ public function exitFirstLevelStatements(): self return $this->scopeOutOfFirstLevelStatement; } - $scope = $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), + /** @var static $scope */ + $scope = ScopeOps::scopeWith( + $this, $this->expressionTypes, $this->nativeExpressionTypes, $this->conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - false, $this->currentlyAssignedExpressions, $this->currentlyAllowedUndefinedExpressions, $this->inFunctionCallsStack, + false, $this->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); $scope->resolvedTypes = $this->resolvedTypes; $scope->truthyScopes = $this->truthyScopes; @@ -3711,8 +3424,8 @@ public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals = $ourExpressionTypes = $this->expressionTypes; $theirExpressionTypes = $otherScope->expressionTypes; - $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); - $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions); + $mergedExpressionTypes = ScopeOps::mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes); + $conditionalExpressions = ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $otherScope->conditionalExpressions); if ($preserveVacuousConditionals) { $conditionalExpressions = $this->preserveVacuousConditionalExpressions( $conditionalExpressions, @@ -3725,111 +3438,41 @@ public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals = $ourExpressionTypes, ); } - $conditionalExpressions = $this->createConditionalExpressions( + $conditionalExpressions = ScopeOps::createConditionalExpressions( $conditionalExpressions, $ourExpressionTypes, $theirExpressionTypes, $mergedExpressionTypes, ); - $conditionalExpressions = $this->createConditionalExpressions( + $conditionalExpressions = ScopeOps::createConditionalExpressions( $conditionalExpressions, $theirExpressionTypes, $ourExpressionTypes, $mergedExpressionTypes, ); - $filter = static function (ExpressionTypeHolder $expressionTypeHolder) { - if ($expressionTypeHolder->getCertainty()->yes()) { - return true; - } - - $expr = $expressionTypeHolder->getExpr(); - - return $expr instanceof Variable - || $expr instanceof FuncCall - || $expr instanceof VirtualNode; - }; - - $mergedExpressionTypes = array_filter($mergedExpressionTypes, $filter); - - $ourNativeExpressionTypes = $this->nativeExpressionTypes; - $theirNativeExpressionTypes = $otherScope->nativeExpressionTypes; - $mergedNativeExpressionTypes = []; - foreach ($ourNativeExpressionTypes as $exprString => $expressionTypeHolder) { - if (!array_key_exists($exprString, $theirNativeExpressionTypes)) { - continue; - } - if (!array_key_exists($exprString, $ourExpressionTypes)) { - continue; - } - if (!array_key_exists($exprString, $theirExpressionTypes)) { - continue; - } - if (!$expressionTypeHolder->equals($ourExpressionTypes[$exprString])) { - continue; - } - if (!$theirNativeExpressionTypes[$exprString]->equals($theirExpressionTypes[$exprString])) { - continue; - } - if (!array_key_exists($exprString, $mergedExpressionTypes)) { - continue; - } - $mergedNativeExpressionTypes[$exprString] = $mergedExpressionTypes[$exprString]; - unset($ourNativeExpressionTypes[$exprString]); - unset($theirNativeExpressionTypes[$exprString]); - } + [$mergedExpressionTypes, $mergedNativeTypes] = ScopeOps::finishMerge( + $mergedExpressionTypes, + $ourExpressionTypes, + $theirExpressionTypes, + $this->nativeExpressionTypes, + $otherScope->nativeExpressionTypes, + ); - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), + /** @var static */ + return ScopeOps::scopeWith( + $this, $mergedExpressionTypes, - array_merge($mergedNativeExpressionTypes, array_filter($this->mergeVariableHolders($ourNativeExpressionTypes, $theirNativeExpressionTypes), $filter)), + $mergedNativeTypes, $conditionalExpressions, - $this->inClosureBindScopeClasses, - $this->anonymousFunctionReflection, - $this->inFirstLevelStatement, [], [], [], + $this->inFirstLevelStatement, $this->afterExtractCall && $otherScope->afterExtractCall, - $this->parentScope, - $this->nativeTypesPromoted, ); } - /** - * @param array $otherConditionalExpressions - * @return array - */ - private function intersectConditionalExpressions(array $otherConditionalExpressions): array - { - $newConditionalExpressions = []; - foreach ($this->conditionalExpressions as $exprString => $holders) { - if (!array_key_exists($exprString, $otherConditionalExpressions)) { - continue; - } - - $otherHolders = $otherConditionalExpressions[$exprString]; - $intersectedHolders = []; - foreach ($holders as $key => $holder) { - if (!array_key_exists($key, $otherHolders)) { - continue; - } - $intersectedHolders[$key] = $holder; - } - - if (count($intersectedHolders) === 0) { - continue; - } - - $newConditionalExpressions[$exprString] = $intersectedHolders; - } - - return $newConditionalExpressions; - } - /** * @param array $currentConditionalExpressions * @param array $sourceConditionalExpressions @@ -3891,220 +3534,6 @@ private function mergeConditionalExpressions(array $newConditionalExpressions, a return $result; } - /** - * @param array $conditionalExpressions - * @param array $ourExpressionTypes - * @param array $theirExpressionTypes - * @param array $mergedExpressionTypes - * @return array - */ - private function createConditionalExpressions( - array $conditionalExpressions, - array $ourExpressionTypes, - array $theirExpressionTypes, - array $mergedExpressionTypes, - ): array - { - $newVariableTypes = $ourExpressionTypes; - - // When our-branch type is a subtype of their-branch type, the union - // absorbs it (merged === their). Such a variable is a poor *guard* — - // asserting its our-branch type later wouldn't reliably select this - // branch — but it remains a valid conditional *target*, so only exclude - // it from guard selection instead of dropping it entirely. - $guardsToExclude = []; - foreach ($theirExpressionTypes as $exprString => $holder) { - if (!array_key_exists($exprString, $mergedExpressionTypes)) { - continue; - } - - if (!$mergedExpressionTypes[$exprString]->equalTypes($holder)) { - continue; - } - - if ( - array_key_exists($exprString, $newVariableTypes) - && !$newVariableTypes[$exprString]->getCertainty()->equals($holder->getCertainty()) - && $newVariableTypes[$exprString]->equalTypes($holder) - ) { - continue; - } - - $guardsToExclude[$exprString] = true; - } - - $typeGuards = []; - foreach ($newVariableTypes as $exprString => $holder) { - if ($holder->getExpr() instanceof VirtualNode) { - continue; - } - if (!array_key_exists($exprString, $mergedExpressionTypes)) { - continue; - } - if (!$holder->getCertainty()->yes()) { - continue; - } - if (array_key_exists($exprString, $guardsToExclude)) { - continue; - } - - if ( - array_key_exists($exprString, $theirExpressionTypes) - && !$theirExpressionTypes[$exprString]->getCertainty()->yes() - ) { - continue; - } - - if ($mergedExpressionTypes[$exprString]->equalTypes($holder)) { - continue; - } - - $typeGuards[$exprString] = $holder; - } - - if (count($typeGuards) === 0) { - return $conditionalExpressions; - } - - // Both isSuperTypeOf() checks below depend only on the guard (and its - // their-branch type), not on the target $exprString, so their results are - // invariant across the target loop. Cache them per guard to avoid - // recomputing expensive supertype checks on big union / constant-array - // types once per (target, guard) pair. - $guardIsSuperTypeOfTheirExprCache = []; - $theirExprIsSuperTypeOfGuardCache = []; - - foreach ($newVariableTypes as $exprString => $holder) { - if ($holder->getExpr() instanceof VirtualNode) { - continue; - } - if ( - array_key_exists($exprString, $mergedExpressionTypes) - && $mergedExpressionTypes[$exprString]->equals($holder) - ) { - continue; - } - - $variableTypeGuards = $typeGuards; - unset($variableTypeGuards[$exprString]); - - if (count($variableTypeGuards) === 0) { - continue; - } - - $exprIsGuardExcluded = array_key_exists($exprString, $guardsToExclude); - foreach ($variableTypeGuards as $guardExprString => $guardHolder) { - // A subtype-absorbed target (kept only for re-narrowing) paired with a - // constant-array guard never helps: such a guard represents a unique - // literal value that is not re-asserted as a condition later, yet the - // downstream isSuperTypeOf() guard machinery pays to compare these - // (potentially huge) constant arrays on every branch merge. Skip them. - if ($exprIsGuardExcluded && $guardHolder->getType()->isConstantArray()->yes()) { - continue; - } - - if ( - array_key_exists($guardExprString, $theirExpressionTypes) - && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() - ) { - $guardIsSuperTypeOfTheirExpr = $guardIsSuperTypeOfTheirExprCache[$guardExprString] ??= $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType()); - - // The reverse isSuperTypeOf() check is expensive on big union / - // constant-array types, so it is evaluated last and only when the - // cheaper forward-based conditions did not already decide. - if ( - $guardIsSuperTypeOfTheirExpr->yes() - || ( - array_key_exists($exprString, $theirExpressionTypes) - && $theirExpressionTypes[$exprString]->getCertainty()->yes() - && !$guardIsSuperTypeOfTheirExpr->no() - ) - || ( - !array_key_exists($exprString, $theirExpressionTypes) - && $holder->getType()->equals($guardHolder->getType()) - && !$guardIsSuperTypeOfTheirExpr->no() - ) - || ($theirExprIsSuperTypeOfGuardCache[$guardExprString] ??= $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType()))->yes() - ) { - continue; - } - } - - $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder); - $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; - } - } - - foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { - if (array_key_exists($exprString, $ourExpressionTypes)) { - continue; - } - - foreach ($typeGuards as $guardExprString => $guardHolder) { - $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); - $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; - } - } - - return $conditionalExpressions; - } - - /** - * @param array $ourVariableTypeHolders - * @param array $theirVariableTypeHolders - * @return array - */ - private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array - { - $intersectedVariableTypeHolders = []; - $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name); - $nodeFinder = new NodeFinder(); - foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { - if (isset($theirVariableTypeHolders[$exprString])) { - if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) { - $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder; - continue; - } - - $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]); - } else { - $expr = $variableTypeHolder->getExpr(); - - $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); - if ($containsSuperGlobal === null) { - $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; - $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); - } - if ($containsSuperGlobal === true) { - continue; - } - - $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType()); - } - } - - foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) { - if (isset($intersectedVariableTypeHolders[$exprString])) { - continue; - } - - $expr = $variableTypeHolder->getExpr(); - - $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); - if ($containsSuperGlobal === null) { - $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; - $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); - } - if ($containsSuperGlobal === true) { - continue; - } - - $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType()); - } - - return $intersectedVariableTypeHolders; - } - public function mergeInitializedProperties(self $calledMethodScope): self { $scope = $this; @@ -4150,7 +3579,7 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco $finallyScope->nativeExpressionTypes, $originalFinallyScope->nativeExpressionTypes, ), - $this->intersectConditionalExpressions($finallyScope->conditionalExpressions), + ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $finallyScope->conditionalExpressions), $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, @@ -4295,7 +3724,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $this->getNamespace(), $expressionTypes, $nativeTypes, - $this->intersectConditionalExpressions($finalScope->conditionalExpressions), + ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $finalScope->conditionalExpressions), $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, $this->inFirstLevelStatement, @@ -4355,7 +3784,7 @@ private function generalizeVariableTypeHolders( $newVariableTypeHolders = []; foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) { foreach ($generalizedExpressions as $generalizedExprString => $generalizedExpr) { - if (!$this->shouldInvalidateExpression($generalizedExprString, $generalizedExpr, $variableTypeHolder->getExpr(), $variableExprString)) { + if (!ScopeOps::shouldInvalidateExpression($this, $this->exprPrinter, $generalizedExprString, $generalizedExpr, $variableTypeHolder->getExpr(), $variableExprString)) { continue; } diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php new file mode 100644 index 00000000000..6b6fd25d48a --- /dev/null +++ b/src/Analyser/ScopeOps.php @@ -0,0 +1,869 @@ +name instanceof Expr) { + return '$' . $node->name; + } + + $key = $exprPrinter->printExpr($node); + $attributes = $node->getAttributes(); + if ( + $node instanceof Node\FunctionLike + && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && (($attributes['startFilePos'] ?? null) !== null) + ) { + $key .= '/*' . $attributes['startFilePos']; + foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] as $arg) { + $key .= ':' . $exprPrinter->printExpr($arg->value); + } + $key .= '*/'; + } + + if (($attributes[MutatingScope::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) { + $key .= '/*' . MutatingScope::KEEP_VOID_ATTRIBUTE_NAME . '*/'; + } + + return $key; + } + + /** + * The memo-hit fast path of MutatingScope::getType(). Returns the resolved + * type on a cache hit; on a miss returns null and assigns the computed + * node key to $key so the caller can continue without recomputing it. + * + * @param-out string $key + */ + public static function getTypeFromCache(MutatingScope $scope, Expr $node, ?string &$key): ?Type + { + $key = self::nodeKey($node, $scope->getExprPrinter()); + + return $scope->resolvedTypes[$key] ?? null; + } + + /** + * The tracked-expression fast path of MutatingScope::resolveType(): for + * non-Variable/Closure/ArrowFunction nodes whose expressionTypes entry has + * certainty Yes, returns the tracked type; null otherwise. + */ + public static function expressionTypeByKey(MutatingScope $scope, Expr $node, string $exprString): ?Type + { + if ( + $node instanceof Variable + || $node instanceof Expr\Closure + || $node instanceof Expr\ArrowFunction + ) { + return null; + } + + $holder = $scope->expressionTypes[$exprString] ?? null; + if ($holder === null || !$holder->getCertainty()->yes()) { + return null; + } + + return $holder->getType(); + } + + /** + * Mirrors MutatingScope::hasExpressionType(). + */ + public static function hasExpressionType(MutatingScope $scope, Expr $node, ExprPrinter $exprPrinter): TrinaryLogic + { + if ($node instanceof Variable && is_string($node->name)) { + return self::hasVariableType($scope, $node->name); + } + + $exprString = self::nodeKey($node, $exprPrinter); + if (!isset($scope->expressionTypes[$exprString])) { + return TrinaryLogic::createNo(); + } + return $scope->expressionTypes[$exprString]->getCertainty(); + } + + /** + * Mirrors MutatingScope::hasVariableType(). + */ + public static function hasVariableType(MutatingScope $scope, string $variableName): TrinaryLogic + { + if (in_array($variableName, Scope::SUPERGLOBAL_VARIABLES, true)) { + return TrinaryLogic::createYes(); + } + + $varExprString = '$' . $variableName; + if (!isset($scope->expressionTypes[$varExprString])) { + if ($scope->canAnyVariableExist()) { + return TrinaryLogic::createMaybe(); + } + + return TrinaryLogic::createNo(); + } + + return $scope->expressionTypes[$varExprString]->getCertainty(); + } + + /** + * Clone-like scope creation with unchanged context; only the expression + * tables and a couple of flags differ from the given scope. + * + * @param array $expressionTypes + * @param array $nativeExpressionTypes + * @param array $conditionalExpressions + * @param array $currentlyAssignedExpressions + * @param array $currentlyAllowedUndefinedExpressions + * @param list $inFunctionCallsStack + */ + public static function scopeWith( + MutatingScope $scope, + array $expressionTypes, + array $nativeExpressionTypes, + array $conditionalExpressions, + array $currentlyAssignedExpressions, + array $currentlyAllowedUndefinedExpressions, + array $inFunctionCallsStack, + bool $inFirstLevelStatement, + bool $afterExtractCall, + ): MutatingScope + { + return $scope->duplicateWith( + $expressionTypes, + $nativeExpressionTypes, + $conditionalExpressions, + $currentlyAssignedExpressions, + $currentlyAllowedUndefinedExpressions, + $inFunctionCallsStack, + $inFirstLevelStatement, + $afterExtractCall, + ); + } + + /** + * Mirrors the former MutatingScope::mergeVariableHolders(). + * + * @param array $ourVariableTypeHolders + * @param array $theirVariableTypeHolders + * @return array + */ + public static function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array + { + $intersectedVariableTypeHolders = []; + $globalVariableCallback = static fn (Node $node) => $node instanceof Variable && is_string($node->name) && in_array($node->name, Scope::SUPERGLOBAL_VARIABLES, true); + $nodeFinder = new NodeFinder(); + foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($theirVariableTypeHolders[$exprString])) { + if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) { + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder; + continue; + } + + $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]); + } else { + $expr = $variableTypeHolder->getExpr(); + + $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); + if ($containsSuperGlobal === null) { + $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; + $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); + } + if ($containsSuperGlobal === true) { + continue; + } + + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType()); + } + } + + foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) { + if (isset($intersectedVariableTypeHolders[$exprString])) { + continue; + } + + $expr = $variableTypeHolder->getExpr(); + + $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME); + if ($containsSuperGlobal === null) { + $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null; + $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal); + } + if ($containsSuperGlobal === true) { + continue; + } + + $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType()); + } + + return $intersectedVariableTypeHolders; + } + + /** + * The tail of MutatingScope::mergeWith() after conditional expressions were + * handled: filters the merged expression types and computes the merged + * native expression types. + * + * @param array $mergedExpressionTypes + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $ourNativeExpressionTypes + * @param array $theirNativeExpressionTypes + * @return array{array, array} + */ + public static function finishMerge( + array $mergedExpressionTypes, + array $ourExpressionTypes, + array $theirExpressionTypes, + array $ourNativeExpressionTypes, + array $theirNativeExpressionTypes, + ): array + { + $filter = static function (ExpressionTypeHolder $expressionTypeHolder) { + if ($expressionTypeHolder->getCertainty()->yes()) { + return true; + } + + $expr = $expressionTypeHolder->getExpr(); + + return $expr instanceof Variable + || $expr instanceof FuncCall + || $expr instanceof VirtualNode; + }; + + $mergedExpressionTypes = array_filter($mergedExpressionTypes, $filter); + + $mergedNativeExpressionTypes = []; + foreach ($ourNativeExpressionTypes as $exprString => $expressionTypeHolder) { + if (!array_key_exists($exprString, $theirNativeExpressionTypes)) { + continue; + } + if (!array_key_exists($exprString, $ourExpressionTypes)) { + continue; + } + if (!array_key_exists($exprString, $theirExpressionTypes)) { + continue; + } + if (!$expressionTypeHolder->equals($ourExpressionTypes[$exprString])) { + continue; + } + if (!$theirNativeExpressionTypes[$exprString]->equals($theirExpressionTypes[$exprString])) { + continue; + } + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + $mergedNativeExpressionTypes[$exprString] = $mergedExpressionTypes[$exprString]; + unset($ourNativeExpressionTypes[$exprString]); + unset($theirNativeExpressionTypes[$exprString]); + } + + return [ + $mergedExpressionTypes, + // + instead of array_merge: the key sets are disjoint (matching entries were + // unset from both native maps above), and array_merge would renumber + // integer-coerced expression keys (an expression printing as '5' is stored + // under int key 5), corrupting them. The native implementation preserves keys. + $mergedNativeExpressionTypes + array_filter(self::mergeVariableHolders($ourNativeExpressionTypes, $theirNativeExpressionTypes), $filter), + ]; + } + + /** + * Mirrors the former MutatingScope::intersectConditionalExpressions(). + * + * @param array $ourConditionalExpressions + * @param array $theirConditionalExpressions + * @return array + */ + public static function intersectConditionalExpressions(array $ourConditionalExpressions, array $theirConditionalExpressions): array + { + $newConditionalExpressions = []; + foreach ($ourConditionalExpressions as $exprString => $holders) { + if (!array_key_exists($exprString, $theirConditionalExpressions)) { + continue; + } + + $otherHolders = $theirConditionalExpressions[$exprString]; + $intersectedHolders = []; + foreach ($holders as $key => $holder) { + if (!array_key_exists($key, $otherHolders)) { + continue; + } + $intersectedHolders[$key] = $holder; + } + + if (count($intersectedHolders) === 0) { + continue; + } + + $newConditionalExpressions[$exprString] = $intersectedHolders; + } + + return $newConditionalExpressions; + } + + /** + * Mirrors the former MutatingScope::createConditionalExpressions(). + * + * @param array $conditionalExpressions + * @param array $ourExpressionTypes + * @param array $theirExpressionTypes + * @param array $mergedExpressionTypes + * @return array + */ + public static function createConditionalExpressions( + array $conditionalExpressions, + array $ourExpressionTypes, + array $theirExpressionTypes, + array $mergedExpressionTypes, + ): array + { + $newVariableTypes = $ourExpressionTypes; + + // When our-branch type is a subtype of their-branch type, the union + // absorbs it (merged === their). Such a variable is a poor *guard* — + // asserting its our-branch type later wouldn't reliably select this + // branch — but it remains a valid conditional *target*, so only exclude + // it from guard selection instead of dropping it entirely. + $guardsToExclude = []; + foreach ($theirExpressionTypes as $exprString => $holder) { + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + + if (!$mergedExpressionTypes[$exprString]->equalTypes($holder)) { + continue; + } + + if ( + array_key_exists($exprString, $newVariableTypes) + && !$newVariableTypes[$exprString]->getCertainty()->equals($holder->getCertainty()) + && $newVariableTypes[$exprString]->equalTypes($holder) + ) { + continue; + } + + $guardsToExclude[$exprString] = true; + } + + $typeGuards = []; + foreach ($newVariableTypes as $exprString => $holder) { + if ($holder->getExpr() instanceof VirtualNode) { + continue; + } + if (!array_key_exists($exprString, $mergedExpressionTypes)) { + continue; + } + if (!$holder->getCertainty()->yes()) { + continue; + } + if (array_key_exists($exprString, $guardsToExclude)) { + continue; + } + + if ( + array_key_exists($exprString, $theirExpressionTypes) + && !$theirExpressionTypes[$exprString]->getCertainty()->yes() + ) { + continue; + } + + if ($mergedExpressionTypes[$exprString]->equalTypes($holder)) { + continue; + } + + $typeGuards[$exprString] = $holder; + } + + if (count($typeGuards) === 0) { + return $conditionalExpressions; + } + + // Both isSuperTypeOf() checks below depend only on the guard (and its + // their-branch type), not on the target $exprString, so their results are + // invariant across the target loop. Cache them per guard to avoid + // recomputing expensive supertype checks on big union / constant-array + // types once per (target, guard) pair. + $guardIsSuperTypeOfTheirExprCache = []; + $theirExprIsSuperTypeOfGuardCache = []; + + foreach ($newVariableTypes as $exprString => $holder) { + if ($holder->getExpr() instanceof VirtualNode) { + continue; + } + if ( + array_key_exists($exprString, $mergedExpressionTypes) + && $mergedExpressionTypes[$exprString]->equals($holder) + ) { + continue; + } + + $variableTypeGuards = $typeGuards; + unset($variableTypeGuards[$exprString]); + + if (count($variableTypeGuards) === 0) { + continue; + } + + $exprIsGuardExcluded = array_key_exists($exprString, $guardsToExclude); + foreach ($variableTypeGuards as $guardExprString => $guardHolder) { + // A subtype-absorbed target (kept only for re-narrowing) paired with a + // constant-array guard never helps: such a guard represents a unique + // literal value that is not re-asserted as a condition later, yet the + // downstream isSuperTypeOf() guard machinery pays to compare these + // (potentially huge) constant arrays on every branch merge. Skip them. + if ($exprIsGuardExcluded && $guardHolder->getType()->isConstantArray()->yes()) { + continue; + } + + if ( + array_key_exists($guardExprString, $theirExpressionTypes) + && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() + ) { + $guardIsSuperTypeOfTheirExpr = $guardIsSuperTypeOfTheirExprCache[$guardExprString] ??= $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType()); + + // The reverse isSuperTypeOf() check is expensive on big union / + // constant-array types, so it is evaluated last and only when the + // cheaper forward-based conditions did not already decide. + if ( + $guardIsSuperTypeOfTheirExpr->yes() + || ( + array_key_exists($exprString, $theirExpressionTypes) + && $theirExpressionTypes[$exprString]->getCertainty()->yes() + && !$guardIsSuperTypeOfTheirExpr->no() + ) + || ( + !array_key_exists($exprString, $theirExpressionTypes) + && $holder->getType()->equals($guardHolder->getType()) + && !$guardIsSuperTypeOfTheirExpr->no() + ) + || ($theirExprIsSuperTypeOfGuardCache[$guardExprString] ??= $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType()))->yes() + ) { + continue; + } + } + + $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; + } + } + + foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) { + if (array_key_exists($exprString, $ourExpressionTypes)) { + continue; + } + + foreach ($typeGuards as $guardExprString => $guardHolder) { + $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo())); + $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; + } + } + + return $conditionalExpressions; + } + + /** + * Depth-first pre-order search for the invalidated expression, replacing a + * NodeFinder::findFirst() call - this runs for every (stored expression, + * invalidated expression) pair whose keys pass the substring pre-filter, + * so the traverser/visitor machinery overhead was significant. + * + * @param class-string $expressionToInvalidateClass + */ + private static function containsExpressionToInvalidate(Scope $scope, ExprPrinter $exprPrinter, Node $node, string $expressionToInvalidateClass, string $exprStringToInvalidate): bool + { + if ( + $exprStringToInvalidate === '$this' + && $node instanceof Name + && ( + in_array($node->toLowerString(), ['self', 'static', 'parent'], true) + || ($scope->getClassReflection() !== null && $scope->getClassReflection()->is($scope->resolveName($node))) + ) + ) { + return true; + } + + if ( + $node instanceof $expressionToInvalidateClass + && self::nodeKey($node, $exprPrinter) === $exprStringToInvalidate + ) { + return true; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + if (self::containsExpressionToInvalidate($scope, $exprPrinter, $subNode, $expressionToInvalidateClass, $exprStringToInvalidate)) { + return true; + } + } elseif (is_array($subNode)) { + foreach ($subNode as $subNodeItem) { + if ( + $subNodeItem instanceof Node + && self::containsExpressionToInvalidate($scope, $exprPrinter, $subNodeItem, $expressionToInvalidateClass, $exprStringToInvalidate) + ) { + return true; + } + } + } + } + + return false; + } + + /** + * The scan of MutatingScope::invalidateExpression(): computes the tables + * with the invalidated entries removed, or null when nothing changed. + * + * @param array $expressionTypes + * @param array $nativeExpressionTypes + * @param array $conditionalExpressions + * @return array{array, array, array}|null + */ + public static function invalidateExpressionEntries( + MutatingScope $scope, + ExprPrinter $exprPrinter, + string $exprStringToInvalidate, + Expr $expressionToInvalidate, + bool $requireMoreCharacters, + ?ClassReflection $invalidatingClass, + array $expressionTypes, + array $nativeExpressionTypes, + array $conditionalExpressions, + ): ?array + { + $invalidated = false; + + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!self::shouldInvalidateExpression($scope, $exprPrinter, $exprStringToInvalidate, $expressionToInvalidate, $exprExpr, (string) $exprString, $requireMoreCharacters, $invalidatingClass)) { + continue; + } + + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + $newConditionalExpressions = []; + foreach ($conditionalExpressions as $conditionalExprString => $holders) { + if (count($holders) === 0) { + continue; + } + $firstExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr(); + if (self::shouldInvalidateExpression($scope, $exprPrinter, $exprStringToInvalidate, $expressionToInvalidate, $firstExpr, self::nodeKey($firstExpr, $exprPrinter), $requireMoreCharacters, $invalidatingClass)) { + $invalidated = true; + continue; + } + $filteredHolders = []; + foreach ($holders as $key => $holder) { + $shouldKeep = true; + $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders(); + foreach ($conditionalTypeHolders as $conditionalTypeHolderExprString => $conditionalTypeHolder) { + if (self::shouldInvalidateExpression($scope, $exprPrinter, $exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr(), (string) $conditionalTypeHolderExprString, invalidatingClass: $invalidatingClass)) { + $invalidated = true; + $shouldKeep = false; + break; + } + } + if (!$shouldKeep) { + continue; + } + + $filteredHolders[$key] = $holder; + } + if (count($filteredHolders) <= 0) { + continue; + } + + $newConditionalExpressions[$conditionalExprString] = $filteredHolders; + } + + if (!$invalidated) { + return null; + } + + return [$expressionTypes, $nativeExpressionTypes, $newConditionalExpressions]; + } + + /** + * The scan of MutatingScope::invalidateMethodsOnExpression(): drops tracked + * MethodCall expressions whose var matches the invalidated key, or returns + * null when nothing changed. + * + * @param array $expressionTypes + * @param array $nativeExpressionTypes + * @return array{array, array}|null + */ + public static function invalidateMethodsOnExpression( + ExprPrinter $exprPrinter, + string $exprStringToInvalidate, + array $expressionTypes, + array $nativeExpressionTypes, + ): ?array + { + $invalidated = false; + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + if (!$expr instanceof MethodCall) { + continue; + } + + if (self::nodeKey($expr->var, $exprPrinter) !== $exprStringToInvalidate) { + continue; + } + + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return null; + } + + return [$expressionTypes, $nativeExpressionTypes]; + } + + /** + * Mirrors the former MutatingScope::shouldInvalidateExpression(). + */ + public static function shouldInvalidateExpression(MutatingScope $scope, ExprPrinter $exprPrinter, string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool + { + if ( + $expr instanceof IntertwinedVariableByReferenceWithExpr + && $exprToInvalidate instanceof Variable + && is_string($exprToInvalidate->name) + && ( + $expr->getVariableName() === $exprToInvalidate->name + || self::getIntertwinedRefRootVariableName($expr->getExpr()) === $exprToInvalidate->name + || self::getIntertwinedRefRootVariableName($expr->getAssignedExpr()) === $exprToInvalidate->name + ) + ) { + return false; + } + + if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) { + return false; + } + + // Variables will not contain traversable expressions. skip the NodeFinder overhead + if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) { + return $exprStringToInvalidate === $exprString; + } + + // getNodeKey() is the pretty-printed expression, and the standard printer is + // compositional: the key of any sub-expression appears verbatim as a substring of + // the key of the expression containing it. So if the invalidated expression's key + // does not appear anywhere in this expression's key, this expression cannot contain + // it and we can skip the expensive AST traversal below. + // Carve-outs where that invariant does not hold: + // - '$this' is special-cased in the visitor to also match self/static/parent, + // - PHPStan's virtual nodes (printed as '__phpstan…') use non-compositional printers + // (e.g. a wrapped variable is printed by name, not as '$name'), + // - keys carrying a getNodeKey() suffix ('/*…*/') are not plain substrings. + if ( + $exprStringToInvalidate !== '$this' + && !str_contains($exprStringToInvalidate, '__phpstan') + && !str_contains($exprStringToInvalidate, '/*') + && !str_contains($exprString, '__phpstan') + && !str_contains($exprString, $exprStringToInvalidate) + ) { + return false; + } + + if (!self::containsExpressionToInvalidate($scope, $exprPrinter, $expr, get_class($exprToInvalidate), $exprStringToInvalidate)) { + return false; + } + + if ( + $expr instanceof PropertyFetch + && $requireMoreCharacters + && $scope->isReadonlyPropertyFetch($expr, false) + ) { + return false; + } + + if ( + $invalidatingClass !== null + && $requireMoreCharacters + && $scope->isPrivatePropertyOfDifferentClass($expr, $invalidatingClass) + ) { + return false; + } + + return true; + } + + public static function getIntertwinedRefRootVariableName(Expr $expr): ?string + { + if ($expr instanceof Variable && is_string($expr->name)) { + return $expr->name; + } + if ($expr instanceof Expr\ArrayDimFetch) { + return self::getIntertwinedRefRootVariableName($expr->var); + } + return null; + } + + /** + * The sorted type-specification list of MutatingScope::filterBySpecifiedTypes(). + * + * @param array $sureTypes + * @param array $sureNotTypes + * @return list + */ + public static function buildTypeSpecifications(array $sureTypes, array $sureNotTypes): array + { + $typeSpecifications = []; + foreach ($sureTypes as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => true, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + foreach ($sureNotTypes as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => false, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + + usort($typeSpecifications, static function (array $a, array $b): int { + $length = strlen($a['exprString']) - strlen($b['exprString']); + if ($length !== 0) { + return $length; + } + + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric + }); + + return $typeSpecifications; + } + + /** + * The conditional-expressions fixed-point matching of + * MutatingScope::filterBySpecifiedTypes(). + * + * @param array $conditionalExpressions + * @param array $specifiedExpressions + * @return array{array, array} + */ + public static function matchConditionalExpressions(array $conditionalExpressions, array $specifiedExpressions): array + { + $conditions = []; + $originallySpecifiedExprStrings = $specifiedExpressions; + $prevSpecifiedCount = -1; + while (count($specifiedExpressions) !== $prevSpecifiedCount) { + $prevSpecifiedCount = count($specifiedExpressions); + foreach ($conditionalExpressions as $conditionalExprString => $conditionalExpressionHolders) { + if (array_key_exists($conditionalExprString, $conditions)) { + continue; + } + + // Pass 1: Prefer exact matches + foreach ($conditionalExpressionHolders as $conditionalExpression) { + if ( + $conditionalExpression->getTypeHolder()->getCertainty()->no() + && array_key_exists($conditionalExprString, $originallySpecifiedExprStrings) + ) { + continue; + } + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if ( + !array_key_exists($holderExprString, $specifiedExpressions) + || !$conditionalTypeHolder->equals($specifiedExpressions[$holderExprString]) + ) { + continue 2; + } + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } + + if (array_key_exists($conditionalExprString, $conditions)) { + continue; + } + + // Pass 2: Supertype match. Only runs when Pass 1 found no exact match for this expression. + foreach ($conditionalExpressionHolders as $conditionalExpression) { + if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) { + continue; + } + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if ( + !array_key_exists($holderExprString, $specifiedExpressions) + || !$conditionalTypeHolder->getCertainty()->equals($specifiedExpressions[$holderExprString]->getCertainty()) + || !$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedExpressions[$holderExprString]->getType())->yes() + ) { + continue 2; + } + } + + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } + } + } + + return [$conditions, $specifiedExpressions]; + } + +} diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 67f8d4ea1aa..19a525739af 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -32,6 +32,7 @@ use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeCombinator; use function array_diff_key; use function array_intersect; use function array_key_exists; @@ -199,10 +200,18 @@ public static function postInitializeContainer(Container $container): void ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); PhpVersionStaticAccessor::registerInstance($container->getByType(PhpVersion::class)); ObjectType::resetCaches(); + $container->getService('typeSpecifier'); BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); ReportUnsafeArrayStringKeyCastingToggle::setLevel($container->getParameter('reportUnsafeArrayStringKeyCasting')); + + // Type operations read global state — the toggles above, the reflection provider, + // the PHP version — so a memoized result is only valid for the state it was computed + // under. Clearing must be the LAST step: building the typeSpecifier service runs + // extension constructors that can already perform type operations, and entries + // memoized before the toggles are set would encode the previous container's state. + TypeCombinator::clearCache(); } public function getCurrentWorkingDirectory(): string diff --git a/src/Node/NodeScanner.php b/src/Node/NodeScanner.php new file mode 100644 index 00000000000..7ee6e13c973 --- /dev/null +++ b/src/Node/NodeScanner.php @@ -0,0 +1,48 @@ +getSubNodeNames() as $nodeName) { + $nodeProperty = $node->$nodeName; + + if ($nodeProperty instanceof Node && self::nodeIsOrContainsYield($nodeProperty)) { + return true; + } + + if (!is_array($nodeProperty)) { + continue; + } + + foreach ($nodeProperty as $nodePropertyArrayItem) { + if ($nodePropertyArrayItem instanceof Node && self::nodeIsOrContainsYield($nodePropertyArrayItem)) { + return true; + } + } + } + + return false; + } + +} diff --git a/src/Parser/ParserRunner.php b/src/Parser/ParserRunner.php new file mode 100644 index 00000000000..b7feb722029 --- /dev/null +++ b/src/Parser/ParserRunner.php @@ -0,0 +1,26 @@ +parse() + * for anything else. Without the extension this delegation is all there is. + */ +final class ParserRunner +{ + + /** + * @return Stmt[]|null + */ + public static function parse(Parser $parser, string $sourceCode, ErrorHandler $errorHandler): ?array + { + return $parser->parse($sourceCode, $errorHandler); + } + +} diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 5862e3de682..d23ecb227aa 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -72,7 +72,7 @@ public function parseFile(string $file): array public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); - $nodes = $this->parser->parse($sourceCode, $errorHandler); + $nodes = ParserRunner::parse($this->parser, $sourceCode, $errorHandler); $tokens = $this->parser->getTokens(); if ($errorHandler->hasErrors()) { diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index 713c1502ef2..ef31614e205 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -38,7 +38,7 @@ public function parseFile(string $file): array public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); - $nodes = $this->parser->parse($sourceCode, $errorHandler); + $nodes = ParserRunner::parse($this->parser, $sourceCode, $errorHandler); if ($errorHandler->hasErrors()) { throw new ParserErrorsException($errorHandler->getErrors(), null); } diff --git a/src/Parser/StubParser.php b/src/Parser/StubParser.php index d98a2cc7215..05a192534f4 100644 --- a/src/Parser/StubParser.php +++ b/src/Parser/StubParser.php @@ -38,7 +38,7 @@ public function parseFile(string $file): array public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); - $nodes = $this->parser->parse($sourceCode, $errorHandler); + $nodes = ParserRunner::parse($this->parser, $sourceCode, $errorHandler); if ($errorHandler->hasErrors()) { throw new ParserErrorsException($errorHandler->getErrors(), null); } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index e2768bc6d7e..9cf0c5d6b27 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -10,14 +10,30 @@ use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\Reflection\ConstantNameHelper; +use function array_keys; use function is_file; +use function strtolower; #[GenerateFactory(interface: OptimizedPsrAutoloaderLocatorFactory::class)] final class OptimizedPsrAutoloaderLocator implements SourceLocator { + /** + * Symbol → the first locator known to declare it, harvested from each + * located file's present symbols. Replaces a linear sweep over all known + * locators per lookup (measured: 286k sweep calls for a single hit on a + * partial self-analysis) with one hash access. + * + * @var array + */ + private array $classIndex = []; + /** @var array */ - private array $locators = []; + private array $functionIndex = []; + + /** @var array */ + private array $constantIndex = []; public function __construct( private PsrAutoloaderMapping $mapping, @@ -29,13 +45,12 @@ public function __construct( #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { - foreach ($this->locators as $locator) { - $reflection = $locator->locateIdentifier($reflector, $identifier); - if ($reflection === null) { - continue; + $indexedLocator = $this->findInIndex($identifier); + if ($indexedLocator !== null) { + $reflection = $indexedLocator->locateIdentifier($reflector, $identifier); + if ($reflection !== null) { + return $reflection; } - - return $reflection; } foreach ($this->mapping->resolvePossibleFilePaths($identifier) as $file) { @@ -49,7 +64,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): continue; } - $this->locators[$file] = $locator; + $this->addToIndex($locator); return $reflection; } @@ -57,6 +72,35 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } + private function findInIndex(Identifier $identifier): ?OptimizedSingleFileSourceLocator + { + if ($identifier->isClass()) { + return $this->classIndex[strtolower($identifier->getName())] ?? null; + } + if ($identifier->isFunction()) { + return $this->functionIndex[strtolower($identifier->getName())] ?? null; + } + if ($identifier->isConstant()) { + return $this->constantIndex[ConstantNameHelper::normalize($identifier->getName())] ?? null; + } + + return null; + } + + private function addToIndex(OptimizedSingleFileSourceLocator $locator): void + { + $presentSymbols = $locator->getPresentSymbols(); + foreach (array_keys($presentSymbols['classes']) as $className) { + $this->classIndex[$className] ??= $locator; + } + foreach (array_keys($presentSymbols['functions']) as $functionName) { + $this->functionIndex[$functionName] ??= $locator; + } + foreach (array_keys($presentSymbols['constants']) as $constantName) { + $this->constantIndex[$constantName] ??= $locator; + } + } + /** * @return list */ diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index f6680f016fd..56a792e2fdc 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -52,13 +52,57 @@ private function getVariableCacheKey(string $file): string return sprintf('v2-%s-%s', $fileHash, $this->phpVersion->getVersionString()); } + /** + * All symbols this file declares, lowercased (constants case-normalized). + * + * Populated from the cache or by fetching the file's nodes; used by + * OptimizedPsrAutoloaderLocator to index known locators by symbol instead + * of sweeping them linearly on every lookup. + * + * @internal + * @return array{classes: array, functions: array, constants: array} + */ + public function getPresentSymbols(): array + { + if ($this->presentSymbols !== null) { + return $this->presentSymbols; + } + + $variableCacheKey = $this->getVariableCacheKey($this->fileName); + $presentSymbolsCacheKey = sprintf('osfsl-%s-presentSymbols', $this->fileName); + $cached = $this->cache->load($presentSymbolsCacheKey, $variableCacheKey); + if ($cached !== null) { + return $this->presentSymbols = $cached; + } + + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($this->fileName); + $presentSymbols = [ + 'classes' => [], + 'functions' => [], + 'constants' => [], + ]; + foreach (array_keys($fetchedNodesResult->getClassNodes()) as $className) { + $presentSymbols['classes'][$className] = true; + } + foreach (array_keys($fetchedNodesResult->getFunctionNodes()) as $functionName) { + $presentSymbols['functions'][$functionName] = true; + } + foreach (array_keys($fetchedNodesResult->getConstantNodes()) as $constantName) { + $presentSymbols['constants'][$constantName] = true; + } + + $this->presentSymbols = $presentSymbols; + $this->cache->save($presentSymbolsCacheKey, $variableCacheKey, $presentSymbols); + + return $presentSymbols; + } + #[Override] public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { - $presentSymbolsCacheKey = sprintf('osfsl-%s-presentSymbols', $this->fileName); if ($this->presentSymbols === null) { $variableCacheKey = $this->getVariableCacheKey($this->fileName); - $this->presentSymbols = $this->cache->load($presentSymbolsCacheKey, $variableCacheKey); + $this->presentSymbols = $this->cache->load(sprintf('osfsl-%s-presentSymbols', $this->fileName), $variableCacheKey); } if ($this->presentSymbols !== null) { if ($identifier->isClass()) { @@ -119,7 +163,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } $this->presentSymbols = $presentSymbols; - $this->cache->save($presentSymbolsCacheKey, $variableCacheKey, $presentSymbols); + $this->cache->save(sprintf('osfsl-%s-presentSymbols', $this->fileName), $variableCacheKey, $presentSymbols); } $nodeToReflection = new NodeToReflection(); diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index 955a9ecdd66..e1589495309 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\DependencyInjection\AutowiredService; use function array_keys; use function implode; use function in_array; @@ -16,6 +17,7 @@ * @author Jordi Boggiano * @see https://github.com/composer/composer/pull/10107 */ +#[AutowiredService] final class PhpFileCleaner { diff --git a/src/Reflection/BetterReflection/SourceLocator/SymbolFinderInFiles.php b/src/Reflection/BetterReflection/SourceLocator/SymbolFinderInFiles.php index 1d1fced22c1..e71b2b6aec3 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SymbolFinderInFiles.php +++ b/src/Reflection/BetterReflection/SourceLocator/SymbolFinderInFiles.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\DependencyInjection\AutowiredService; use function array_filter; use function array_slice; use function count; @@ -17,6 +18,7 @@ use function str_contains; use function strtolower; +#[AutowiredService] final class SymbolFinderInFiles { diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index cecedced1d7..0c94a5082c7 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -7,6 +7,7 @@ use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\Node\NodeScanner; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedFunctionVariant; @@ -23,7 +24,6 @@ use PHPStan\Type\TypehintHelper; use function array_map; use function array_reverse; -use function is_array; use function is_string; use function strtolower; @@ -287,33 +287,7 @@ public function acceptsNamedArguments(): TrinaryLogic private function nodeIsOrContainsYield(Node $node): bool { - if ($node instanceof Node\Expr\Yield_) { - return true; - } - - if ($node instanceof Node\Expr\YieldFrom) { - return true; - } - - foreach ($node->getSubNodeNames() as $nodeName) { - $nodeProperty = $node->$nodeName; - - if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) { - return true; - } - - if (!is_array($nodeProperty)) { - continue; - } - - foreach ($nodeProperty as $nodePropertyArrayItem) { - if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) { - return true; - } - } - } - - return false; + return NodeScanner::nodeIsOrContainsYield($node); } public function getAsserts(): Assertions diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 290fa53c7ed..fd544c16f17 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -2,16 +2,72 @@ namespace PHPStan\Turbo; -use PHPStan\Internal\CombinationsHelper; -use function class_alias; +use PhpParser\Node; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\ArrowFunction; +use PhpParser\Node\Expr\CallLike; +use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Expr\UnaryMinus; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; +use PhpParser\Node\FunctionLike; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar; +use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\VariadicPlaceholder; +use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ConditionalExpressionHolder; +use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; +use PHPStan\Node\VirtualNode; +use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ErrorType; +use PHPStan\Type\RecursionGuard; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\VerbosityLevel; +use PHPStanTurbo\Runtime; use function extension_loaded; +use function getenv; +use function phpversion; final class TurboExtensionEnabler { + /** + * The native classes must match the PHP implementations exactly, so the + * extension is only enabled when its version is the expected one. The + * version is the short SHA of the last commit touching turbo-ext/src/, + * enforced by the turbo-ext.yml version job. + */ + public const EXPECTED_EXTENSION_VERSION = 'c229525'; + + private static bool $typeCombinatorCacheEnabled = false; + public static function isLoaded(): bool { - return extension_loaded('phpstanturbo'); + return extension_loaded('phpstan_turbo'); + } + + /** + * Read lazily by TypeCombinator: enableIfLoaded() runs before the Composer + * autoloader, so it cannot touch autoloadable classes itself. + */ + public static function isTypeCombinatorCacheEnabled(): bool + { + return self::$typeCombinatorCacheEnabled; } public static function enableIfLoaded(): void @@ -20,7 +76,72 @@ public static function enableIfLoaded(): void return; } - class_alias('PHPStanTurbo\CombinationsHelper', CombinationsHelper::class); + if (getenv('PHPSTAN_TURBO') === '0') { + return; + } + + if (phpversion('phpstan_turbo') !== self::EXPECTED_EXTENSION_VERSION) { + return; + } + + // Class names the extension needs at runtime, passed as ::class + // constants so they stay correct under the scoped phar. The *Impl + // entries name the classes the extension instantiates — these are the + // stub subclasses loaded below, so that every object satisfies the + // original type hints. + Runtime::configure([ + 'typeCombinator' => TypeCombinator::class, + 'type' => Type::class, + 'recursionGuard' => RecursionGuard::class, + 'booleanType' => BooleanType::class, + 'constantBooleanType' => ConstantBooleanType::class, + 'shouldNotHappenException' => ShouldNotHappenException::class, + 'verbosityLevel' => VerbosityLevel::class, + 'variable' => Variable::class, + 'funcCall' => FuncCall::class, + 'virtualNode' => VirtualNode::class, + 'node' => Node::class, + 'name' => Name::class, + 'expr' => Expr::class, + 'propertyFetch' => PropertyFetch::class, + 'intertwinedVariableByReferenceWithExpr' => IntertwinedVariableByReferenceWithExpr::class, + 'arrayDimFetch' => ArrayDimFetch::class, + 'methodCall' => MethodCall::class, + 'functionLike' => FunctionLike::class, + 'callLike' => CallLike::class, + 'staticCall' => StaticCall::class, + 'newExpr' => New_::class, + 'classStmt' => Class_::class, + 'variadicPlaceholder' => VariadicPlaceholder::class, + 'errorType' => ErrorType::class, + 'scalar' => Scalar::class, + 'arrayExpr' => Array_::class, + 'unaryMinus' => UnaryMinus::class, + 'yield' => Yield_::class, + 'yieldFrom' => YieldFrom::class, + 'stmt' => Stmt::class, + 'nodeVisitorAbstract' => NodeVisitorAbstract::class, + 'closureExpr' => Closure::class, + 'arrowFunction' => ArrowFunction::class, + 'trinaryLogicImpl' => TrinaryLogic::class, + 'expressionTypeHolderImpl' => ExpressionTypeHolder::class, + 'conditionalExpressionHolderImpl' => ConditionalExpressionHolder::class, + ]); + + // Shadow the PHP implementations with stubs extending the extension's + // native classes. The stubs are declared before the Composer autoloader + // registers, so later references to the original names resolve to them. + require_once __DIR__ . '/../../turbo-ext/stubs/TrinaryLogic.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/ExpressionTypeHolder.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/ConditionalExpressionHolder.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/CombinationsHelper.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/NodeTraverser.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/ScopeOps.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/NodeScanner.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/ParserRunner.php'; + require_once __DIR__ . '/../../turbo-ext/stubs/TypeCombinatorCache.php'; + + self::$typeCombinatorCacheEnabled = true; } } diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 79741c0f301..d010d26ad5d 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -403,7 +403,10 @@ public function tryRemove(Type $typeToRemove): ?Type } $bound = TypeCombinator::remove($this->getBound(), $typeToRemove); - if ($this->getBound() === $bound) { + // Value comparison, not identity: remove() returning the very same instance when it + // removes nothing is an implementation detail, not part of its contract. The identity + // check is only a fast path (equals() has no such shortcut). + if ($this->getBound() === $bound || $this->getBound()->equals($bound)) { return null; } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 69be1a0ef22..bc0359bd6ea 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -75,7 +75,16 @@ class IntersectionType implements CompoundType use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - private bool $sortedTypes = false; + /** + * Sorting must not reorder $types: describe() sorts for readability, but $types is the + * type's value — getTypes() exposes it and callers merge array shapes in that order. It + * used to be sorted in place, so describing a union permanently changed what getTypes() + * returned, making an immutable value object's observable state depend on what had been + * called on it before. + * + * @var list|null + */ + private ?array $sortedTypesCache = null; private ?TrinaryLogic $isBoolean = null; @@ -138,14 +147,7 @@ public function getTypes(): array */ private function getSortedTypes(): array { - if ($this->sortedTypes) { - return $this->types; - } - - $this->types = UnionTypeHelper::sortTypes($this->types); - $this->sortedTypes = true; - - return $this->types; + return $this->sortedTypesCache ??= UnionTypeHelper::sortTypes($this->types); } public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index c5603b98a1c..1d4f689dc7e 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -119,6 +119,18 @@ class ObjectType implements TypeWithClassName, SubtractableType private ?string $cachedDescription = null; + /** + * The reflection resolved on demand by getClassReflection(), kept apart from the one + * handed to the constructor. The constructor's reflection is part of the type's value + * — it can differ from what the provider would return (an anonymous class is identified + * by its start line, a final-by-keyword override changes subtyping) — and so it belongs + * in the cache key. A lazily fetched one is merely a cache: folding it into the key made + * describe(VerbosityLevel::cache()) depend on whether the fetch had happened yet, which + * made TypeCombinator::union() — which dedupes on that key — depend on cache warmth + * rather than on its arguments' values. + */ + private ?ClassReflection $lazyClassReflection = null; + /** @var array> */ private static array $enumCases = []; @@ -1783,8 +1795,9 @@ public function traverseSimultaneously(Type $right, callable $cb): Type public function getNakedClassReflection(): ?ClassReflection { - if ($this->classReflection !== null) { - return $this->classReflection; + $classReflection = $this->resolvedClassReflection(); + if ($classReflection !== null) { + return $classReflection; } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -1797,20 +1810,27 @@ public function getNakedClassReflection(): ?ClassReflection public function getClassReflection(): ?ClassReflection { - if ($this->classReflection === null) { + $classReflection = $this->resolvedClassReflection(); + if ($classReflection === null) { $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if (!$reflectionProvider->hasClass($this->className)) { return null; } - $this->classReflection = $reflectionProvider->getClass($this->className); + $classReflection = $this->lazyClassReflection = $reflectionProvider->getClass($this->className); } - if ($this->classReflection->isGeneric()) { - return $this->classReflection->withTypes(array_values($this->classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); + if ($classReflection->isGeneric()) { + return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); } - return $this->classReflection; + return $classReflection; + } + + /** The reflection already known, from either source — never triggers a fetch. */ + private function resolvedClassReflection(): ?ClassReflection + { + return $this->classReflection ?? $this->lazyClassReflection; } public function getAncestorWithClassName(string $className): ?self @@ -1819,7 +1839,8 @@ public function getAncestorWithClassName(string $className): ?self return $this; } - if ($this->classReflection !== null && $className === $this->classReflection->getName()) { + $resolvedClassReflection = $this->resolvedClassReflection(); + if ($resolvedClassReflection !== null && $className === $resolvedClassReflection->getName()) { return $this; } diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index f5545533765..a55ea10fa2d 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -7,7 +7,14 @@ final class RecursionGuard { - /** @var true[] */ + /** + * While this is non-empty, run() and runOnObjectIdentity() short-circuit to ErrorType, + * so a type operation's result depends on the call stack and not only on its arguments. + * The native extension reads this property to know when it must not memoize + * TypeCombinator's operations (see PHPStanTurbo\TypeCombinatorCache). + * + * @var true[] + */ private static array $context = []; /** diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 00b298e8c7f..1d22f9ba33c 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use PHPStan\Turbo\TurboExtensionEnabler; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -55,6 +56,28 @@ final class TypeCombinator { + /** + * Turned on by TurboExtensionEnabler when the native extension is active, so that + * union(), intersect() and remove() route through the memoizing TypeCombinatorCache. + * Without the extension the operations run exactly as they always have — the flag + * costs a property read, and nothing else changes. + */ + private static ?bool $cacheEnabled = null; + + /** + * Memoization hands back shared Type instances, and a Type lazily resolves a + * ClassReflection belonging to the container that created it — so the cache must + * not outlive its container. + */ + public static function clearCache(): void + { + if ((self::$cacheEnabled ??= TurboExtensionEnabler::isTypeCombinatorCacheEnabled()) === false) { + return; + } + + TypeCombinatorCache::clearCache(); + } + public static function addNull(Type $type): Type { $nullType = new NullType(); @@ -67,6 +90,16 @@ public static function addNull(Type $type): Type } public static function remove(Type $fromType, Type $typeToRemove): Type + { + if (self::$cacheEnabled ??= TurboExtensionEnabler::isTypeCombinatorCacheEnabled()) { + return TypeCombinatorCache::remove($fromType, $typeToRemove); + } + + return self::doRemove($fromType, $typeToRemove); + } + + /** @internal Delegated to from TypeCombinatorCache, which the native extension shadows to memoize it. */ + public static function doRemove(Type $fromType, Type $typeToRemove): Type { if ($typeToRemove instanceof UnionType) { foreach ($typeToRemove->getTypes() as $unionTypeToRemove) { @@ -154,6 +187,16 @@ public static function containsNull(Type $type): bool } public static function union(Type ...$types): Type + { + if (self::$cacheEnabled ??= TurboExtensionEnabler::isTypeCombinatorCacheEnabled()) { + return TypeCombinatorCache::union(...$types); + } + + return self::doUnion(...$types); + } + + /** @internal Delegated to from TypeCombinatorCache, which the native extension shadows to memoize it. */ + public static function doUnion(Type ...$types): Type { $typesCount = count($types); if ($typesCount === 0) { @@ -1593,6 +1636,16 @@ private static function finiteUnionMembers(UnionType $union): ?array } public static function intersect(Type ...$types): Type + { + if (self::$cacheEnabled ??= TurboExtensionEnabler::isTypeCombinatorCacheEnabled()) { + return TypeCombinatorCache::intersect(...$types); + } + + return self::doIntersect(...$types); + } + + /** @internal Delegated to from TypeCombinatorCache, which the native extension shadows to memoize it. */ + public static function doIntersect(Type ...$types): Type { $typesCount = count($types); if ($typesCount === 0) { diff --git a/src/Type/TypeCombinatorCache.php b/src/Type/TypeCombinatorCache.php new file mode 100644 index 00000000000..232041c9465 --- /dev/null +++ b/src/Type/TypeCombinatorCache.php @@ -0,0 +1,41 @@ + [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException ]; - private bool $sortedTypes = false; + /** + * Sorting must not reorder $types: describe() sorts for readability, but $types is the + * type's value — getTypes() exposes it and callers merge array shapes in that order. It + * used to be sorted in place, so describing a union permanently changed what getTypes() + * returned, making an immutable value object's observable state depend on what had been + * called on it before. + * + * @var list|null + */ + private ?array $sortedTypesCache = null; /** @var array */ private array $cachedDescriptions = []; @@ -134,14 +143,7 @@ public function isNormalized(): bool */ protected function getSortedTypes(): array { - if ($this->sortedTypes) { - return $this->types; - } - - $this->types = UnionTypeHelper::sortTypes($this->types); - $this->sortedTypes = true; - - return $this->types; + return $this->sortedTypesCache ??= UnionTypeHelper::sortTypes($this->types); } public function getReferencedClasses(): array diff --git a/tests/PHPStan/Analyser/nsrt/bug-12575.php b/tests/PHPStan/Analyser/nsrt/bug-12575.php index f5199523d44..08892d0563f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12575.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12575.php @@ -34,14 +34,14 @@ public function doFoo(): void { assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this->add(A::class)); assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); - assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + assertType('T of object (class Bug12575\Bar, parameter)', $this->getT()); } public function doBar(): void { $this->add(B::class); assertType('$this(Bug12575\Bar)&static(Bug12575\Bar)', $this); - assertType('T of object (class Bug12575\Bar, argument)', $this->getT()); + assertType('T of object (class Bug12575\Bar, parameter)', $this->getT()); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-yield-oversized-self-rejection.php b/tests/PHPStan/Analyser/nsrt/bug-yield-oversized-self-rejection.php index c5185f7af3c..9f8c5844583 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-yield-oversized-self-rejection.php +++ b/tests/PHPStan/Analyser/nsrt/bug-yield-oversized-self-rejection.php @@ -90,7 +90,7 @@ function build(string $eventClass): array ]; } - assertType("non-empty-array&oversized-array", $r); + assertType("non-empty-array&oversized-array", $r); return $r; } diff --git a/turbo-ext/.gitignore b/turbo-ext/.gitignore index 0106ec51977..e4d0914c737 100644 --- a/turbo-ext/.gitignore +++ b/turbo-ext/.gitignore @@ -1,3 +1,6 @@ -/vendor -/ext -/.zephir +/PHP-CPP/ +/src/*.o +/src/parser/*.o +/phpstan_turbo.so +/side-by-side.html +/version.stamp diff --git a/turbo-ext/CLAUDE.md b/turbo-ext/CLAUDE.md index 50a3da24453..3866ad1a816 100644 --- a/turbo-ext/CLAUDE.md +++ b/turbo-ext/CLAUDE.md @@ -1,1920 +1,152 @@ -# PHPStan Turbo Extension - Comprehensive Developer Guide - -## Overview - -The **PHPStan Turbo Extension** is an experimental project aimed at improving PHPStan's performance by rewriting performance-critical parts of PHPStan as a native PHP extension using the [Zephir language](https://zephir-lang.com/). - -**Status:** Highly experimental work-in-progress (Proof of Concept) - -## Purpose & Goals - -### Primary Goal -Accelerate PHPStan's analysis by moving computationally expensive operations from userland PHP into compiled C code via a PHP extension. - -### Current Focus -The initial implementation targets the `CombinationsHelper::combinations()` method, which is used extensively in type inference for: -- Constant array type operations (`src/Type/Constant/ConstantArrayType.php:1829`) -- sprintf() function return type analysis (`src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php`) -- implode() function return type analysis (`src/Type/Php/ImplodeFunctionReturnTypeExtension.php`) - -### Why This Matters -The `combinations()` method generates Cartesian products of arrays, which can be computationally expensive during type inference. Moving this to native code can provide significant performance improvements. - -## Architecture - -### Integration Strategy - -PHPStan uses a **transparent fallback mechanism** that automatically switches to the native implementation when the extension is available: - -1. **Default Implementation**: Pure PHP implementation in `src/Internal/CombinationsHelper.php` -2. **Turbo Implementation**: Native code in `turbo-ext/phpstanturbo/CombinationsHelper.zep` -3. **Auto-Detection**: `src/Turbo/TurboExtensionEnabler.php` checks if the extension is loaded -4. **Class Aliasing**: When loaded, the Zephir class `PHPStanTurbo\CombinationsHelper` replaces `PHPStan\Internal\CombinationsHelper` - -### Initialization Flow - -``` -bin/phpstan (line 21) - ↓ -TurboExtensionEnabler::enableIfLoaded() - ↓ -extension_loaded('phpstanturbo') → true? - ↓ YES -class_alias('PHPStanTurbo\CombinationsHelper', 'PHPStan\Internal\CombinationsHelper') - ↓ -All subsequent calls to CombinationsHelper::combinations() use native code -``` - -This also applies to the test suite via `tests/bootstrap.php:8`. - -## Directory Structure - -``` -turbo-ext/ -├── README.md # User-facing documentation -├── CLAUDE.md # This file - comprehensive developer guide -├── .gitignore # Excludes /vendor, /ext, /.zephir -├── composer.json # Requires phalcon/zephir ^0.19.0 -├── composer.lock # Locked dependencies -├── config.json # Zephir compiler configuration -└── phpstanturbo/ # Zephir source code directory - └── CombinationsHelper.zep # Native implementation of combinations() - -Generated during build (git-ignored): -├── vendor/ # Composer dependencies (Zephir) -├── ext/ # Generated C code and compiled extension -│ └── modules/phpstanturbo.so # The final compiled extension -└── .zephir/ # Zephir build cache -``` - ---- - -# Comprehensive Zephir Language Guide - -## What is Zephir? - -Zephir (Z(end) E(ngine)/PH(P)/I(nte)r(mediate), pronounced "zaefire") is an open-source, domain-specific language designed to simplify PHP extension development. It addresses the needs of PHP developers who want to write and compile code executable by PHP while emphasizing type and memory safety. - -### Core Philosophy - -**Zephir was not created to replace PHP or C. Instead, it is a complement to them.** It targets the specific use case where base libraries rarely change but must be highly functional and fast—making compilation's overhead worthwhile. - -### Key Motivations - -1. **Performance Enhancement**: PHP applications need to balance "stability, performance, and functionality," but base frameworks and libraries face performance limitations due to high levels of abstraction. Dynamic languages like PHP struggle with optimization because compilers have fewer type clues available compared to static languages. - -2. **Developer Accessibility**: Rather than forcing PHP developers to learn C, Zephir provides a middle ground. It combines static typing benefits with dynamic language familiarity. - -3. **Strategic Positioning**: It enables developers to "implement object-oriented libraries/frameworks/applications that can be used from PHP, gaining important seconds" through compilation. - -4. **Code Protection**: Compilation produces native binaries that obscure original source code from users. - -5. **Selective Optimization**: Developers choose which application portions warrant compilation overhead. - -### Key Features - -| Feature | Capability | -|---------|-----------| -| **Type System** | Supports both dynamic and static typing | -| **Memory Safety** | Prohibits pointers or direct memory management | -| **Compilation** | Ahead-of-time compilation to C code | -| **Memory Model** | Task-local garbage collection with automatic management | - -### How It Works - -Zephir code is organized in namespaced classes. Variables must be explicitly declared with types—either `var` for dynamic variables or specific types like `int` for static variables. The compiler transparently converts Zephir code to C, which is then compiled by gcc, clang, or vc++. - -The compilation process optimizes static variables and removes redundant operations, while developers need not understand C internals to write effective Zephir code. - ---- - -## Installation & Prerequisites - -### System Requirements - -**Core Requirements:** -- Zephir parser >= 1.3.0 -- C compiler (gcc >= 4.4, clang >= 3.0, Visual C++ >= 11, or Intel C++) -- re2c 0.13.6 or later -- PHP development headers and tools - -**Linux-Specific Requirements:** -- GNU make 3.81 or later -- autoconf 2.31 or later -- automake 1.14 or later -- libpcre3 -- build-essential package (for Ubuntu/Debian systems using gcc) - -### Ubuntu/Debian Installation - -```bash -sudo apt-get update -sudo apt-get install git gcc make re2c php php-json php-dev libpcre3-dev build-essential -``` - -### Verification Steps - -1. Confirm PHP version: - ```bash - php -v - ``` - -2. Verify PHP development libraries: - ```bash - phpize -v - ``` - -### Installing Zephir - -**Three installation methods are available:** - -1. **Composer (Recommended)** - ```bash - # Project-level - composer require phalcon/zephir - composer exec zephir help - - # Global - composer global require phalcon/zephir - zephir help - ``` - -2. **PHAR Release** - - Download from GitHub releases and place in your PATH - -3. **Git Clone** - - Clone the repository with dependencies, then create a symlink or reference the binary directly - -### Post-Installation - -Verify successful setup: -```bash -zephir help -``` - ---- - -## Zephir Language Fundamentals - -### File Organization - -Zephir enforces strict file structure: **every file must contain a class (and just one class).** The directory layout must align with namespace and class names, similar to PSR-4 standards but mandatory by the language itself. - -The directory structure must match the names of the classes and namespaces used. The compiler raises an exception if the file location or class declaration doesn't match expected patterns. - -### Code Organization - -Classes require explicit namespaces. Example: - -```zephir -namespace PHPStanTurbo; - -class CombinationsHelper -{ - // class content -} -``` - -### Instructions & Syntax - -- Statements can be separated using semicolons (like Java, C/C++, PHP), though they're optional -- Multiple expressions can appear on a single line -- No dollar signs required for variables -- The `let` keyword is used for variable assignment (variables are immutable by default) -- Optional parentheses in control structures - -### Comments - -Two styles are supported: -- Single-line comments: `//` -- Multi-line comments: `/* */` - -**Important:** Multi-line comments are also used as docblocks, and they're exported to the generated code, making them integral to the language rather than merely decorative. - -### Variable Declarations - -All variables must be explicitly declared within their scope using the `var` keyword or by specifying a type. Variables can optionally have an initial compatible default value. Names are case-sensitive and cannot be reserved words. - -```zephir -var a, b, c; -let a = "hello"; - -int counter = 0; -string name = "PHPStan"; -``` - -### Variable Scope - -Variables are locally scoped to the method where they were declared, preventing cross-method access. - -### Super Globals - -Direct global variables aren't supported, but you can access PHP's super-globals like `_POST` and `_SERVER` using standard bracket notation syntax: - -```zephir -let value = _POST["username"]; -``` - ---- - -## Zephir Type System - -### Dynamic Types - -Variables declared with the `var` keyword can be reassigned to different types freely: - -```zephir -var a, b, c; -let a = "hello", b = false; -let a = 10; // reassignment to different type -``` - -Dynamic variables support **eight types**: - -1. **Array**: An ordered map that associates values to keys -2. **Boolean**: Can be either `true` or `false` -3. **Float/Double**: Platform-dependent floating-point numbers -4. **Integer**: Platform-dependent signed integers -5. **Null**: Represents a variable with no value -6. **Object**: PHP-style object abstraction -7. **Resource**: Holds a reference to an external resource -8. **String**: A series of characters, where a character is the same as a byte - -#### Dynamic Type Details - -**Arrays:** -- Use square bracket syntax: `[1, 2, 3]` -- Key-value pairs use double colons: `["key": value]` -- Only long and string values function as keys - -**Strings:** -- **Must use double quotes** (unlike PHP where single quotes also work) -- Support escape sequences (`\t`, `\n`, `\r`, `\\`, `\"`) -- Variable interpolation isn't supported; concatenation is required instead -- **PHP to Zephir**: Convert all single-quoted PHP strings (`'text'`) to double-quoted (`"text"`) - -### Static Types - -Static typing prevents type changes after declaration, enabling compiler optimization. Supported static types include: - -- Basic types: `boolean`, `char`, `integer`, `long`, `string` -- Unsigned variants: `unsigned char`, `unsigned integer`, `unsigned long` -- `float`/`double`, `array` - -#### Static Type Conversion Behaviors - -**Boolean:** -- Values auto-cast -- Non-zero numbers become `true` -- Zero/null/false become `false` -- String assignment throws compiler exceptions - -**Char:** -- Stores single characters using **single-quote syntax** (`'Z'`) -- **Important**: Accessing a string with array/index access returns a `char` type -- Extracted from strings via indexing: `let ch = str[0];` (ch is char type) -- Must use single quotes for char literals: `'A'`, `'z'`, `'\n'` -- Can be converted to string by concatenation or assignment to string variable - -**Integer/Unsigned Integer:** -- Numeric values auto-cast (floats truncate) -- Null/false become 0 -- Strings cause compiler errors -- Unsigned variants reject negative assignments - -**Long/Unsigned Long:** -- Twice the size of standard integers -- Similar auto-casting rules to integer types - -**String:** -- **Must use double-quote syntax** (`"hello"`) -- **Important**: Accessing a string index with `str[i]` returns a `char` (not a string) -- Characters auto-convert to strings when assigned to string variables -- Null becomes empty string -- Cannot use single quotes for strings (single quotes are for char type only) - -### String vs Char: Key Distinctions - -Understanding the difference between `string` and `char` types is critical in Zephir: - -**Syntax Difference:** -- **String**: Uses double quotes `"hello"` -- **Char**: Uses single quotes `'h'` - -**Important for PHP to Zephir Conversion:** -When converting PHP code to Zephir, **all single-quoted strings must be converted to double-quoted strings** and properly escaped following the same rules as double-quoted strings in PHP: - -```php -// PHP - both single and double quotes work for strings -$str1 = 'hello world'; -$str2 = "hello world"; -$str3 = 'it\'s a test'; -``` - -```zephir -// Zephir - MUST use double quotes for strings -string str1 = "hello world"; -string str2 = "hello world"; -string str3 = "it's a test"; // No need to escape single quote in double-quoted string -``` - -Remember: In Zephir, single quotes create a `char` type (single character), not a `string`! - -**Array Access Returns Char:** -When you access a string by index, Zephir returns a `char`, not a `string`: - -```zephir -string text = "hello"; -char firstChar; -let firstChar = text[0]; // firstChar = 'h' (char type, not string) -``` - -**Practical Example:** -```zephir -string str = "hello"; -char ch; - -// Iterating over a string yields char types -for ch in str { - // ch is of type char ('h', 'e', 'l', 'l', 'o') - // Use single-quote syntax for comparison - if ch == 'e' { - echo "Found 'e'"; - } -} - -// Array access also returns char -let ch = str[0]; // ch = 'h' (char type) -``` - -**Type Conversion:** -```zephir -char ch = 'A'; -string str; - -// Char to String: Auto-converts when assigned to string -let str = ch; // str = "A" - -// String to Char: Use array access -let str = "Hello"; -let ch = str[0]; // ch = 'H' -``` - -**Common Pitfall:** -```zephir -string text = "test"; - -// WRONG: This won't work as expected -// text[0] returns char 't', not string "t" -if text[0] == "t" { // Comparing char to string - type mismatch! - // This may not work as expected -} - -// CORRECT: Use single quotes for char comparison -if text[0] == 't' { // Comparing char to char - correct! - // This works correctly -} -``` - ---- - -## Operators - -### Arithmetic Operators - -- Negation: `-a` -- Addition: `a + b` -- Subtraction: `a - b` -- Multiplication: `a * b` -- Division: `a / b` -- Modulus: `a % b` - -### Bitwise Operators - -- And: `a & b` -- Or: `a | b` -- Xor: `a ^ b` -- Not: `~a` -- Left shift: `a << b` -- Right shift: `a >> b` - -### Comparison Operators - -Type-aware comparisons: -- Equality: `==` -- Identity: `===` -- Inequality: `!=`, `<>` -- Non-identity: `!==` -- Relational: `<`, `>`, `<=`, `>=` - -Behavior depends on variable types; dynamic variables follow PHP conventions. - -### Logical Operators - -- And: `&&` -- Or: `||` -- Not: `!` - -### Special Operators - -**Empty**: Checks if an expression is `null`, empty string, or empty array. - -```zephir -if empty str { - // str is empty -} -``` - -**Fetch**: Reduces a common operation in PHP into a single instruction by checking array key existence and assigning the value simultaneously. - -```zephir -if fetch value, array["key"] { - // value is now assigned if key exists -} -``` - -**Isset**: Verifies property or index definition; behaves like PHP's `array_key_exists()`, returning true even for null values. - -**Typeof**: Determines variable type through comparison or retrieval. - -```zephir -if typeof str == "string" { - // str is a string -} -``` - -**Type Hints**: -- Weak hints (``) guide compiler checks -- Strict hints (``) enforce runtime validation - -**Branch Prediction Hints**: `unlikely` keyword optimizes processor branch prediction for rarely-executed code paths. - -```zephir -if unlikely errorCondition { - // rarely executed error handling -} -``` - -### Operator Precedence - -**Precedence** determines parsing order: operators with higher precedence become the operands of operators with lower precedence. - -**Associativity** governs grouping when operators have equal precedence. The minus operator is left-associative, so `1 - 2 - 3` evaluates as `(1 - 2) - 3` = -4. Assignment is right-associative: `let a = b = c` groups as `let a = (b = c)`. - -**Non-associative operators** cannot be adjacent. The `new` operator is the only non-associative operator in Zephir. - -| Precedence | Operators | Type | Associativity | -|---|---|---|---| -| 1 | `->` | Member Access | right-to-left | -| 2 | `~` | Bitwise NOT | right-to-left | -| 3 | `!` | Logical NOT | right-to-left | -| 4 | `new` | new | non-associative | -| 5 | `clone` | clone | right-to-left | -| 6 | `typeof` | Type-of | right-to-left | -| 7 | `..`, `...` | Range | left-to-right | -| 8 | `isset`, `fetch`, `empty` | Special | right-to-left | -| 9 | `*`, `/`, `%` | Arithmetic | left-to-right | -| 10 | `+`, `-`, `.` | Arithmetic/Concat | left-to-right | -| 11 | `<<`, `>>` | Bitwise shift | left-to-right | -| 12-13 | Comparison operators | Comparison | left-to-right | -| 14-16 | Bitwise operators | Bitwise | left-to-right | -| 17-19 | Logical operators | Logical | left-to-right | -| 20-21 | `likely`, `unlikely`, `?`, `=>` | Branch/Logical | right-to-left | - ---- - -## Control Structures - -### Conditionals - -**If Statement:** -Evaluates expressions with required braces. Supports `else` and `elseif` clauses. Parentheses in the evaluated expression are optional. - -```zephir -if count == 0 { - return [[]]; -} - -if a == 10 { - echo "ten"; -} elseif a == 20 { - echo "twenty"; -} else { - echo "other"; -} -``` - -**Switch Statement:** -Evaluates an expression against a series of predefined literal values, executing the corresponding `case` block or falling back to the `default` block case. - -```zephir -switch value { - case 1: - echo "one"; - break; - case 2: - echo "two"; - break; - default: - echo "other"; -} -``` - -### Loops - -**While Statement:** -Iterates as long as its given condition evaluates as `true`. - -```zephir -while index < 10 { - let index++; -} -``` - -**Loop Statement:** -Creates infinite loops that require explicit `break` statements to exit. - -```zephir -loop { - if condition { - break; - } -} -``` - -**For Statement:** -Traverses arrays, strings, and ranges. Supports: -- Basic iteration: `for item in array` -- Key-value pairs: `for key, value in items` -- Reverse traversal: `for value in reverse items` -- Anonymous variables using `_` placeholder to suppress unused variable warnings - -```zephir -for ch in str { - // iterate over each character -} - -for elem in head { - // iterate over array elements -} - -for key, value in items { - // iterate with keys and values -} - -for value in reverse items { - // iterate in reverse order -} -``` - -**Break Statement:** -Ends execution of the current `while`, `for` or `loop` statement. - -**Continue Statement:** -Skips the rest of the current loop iteration and continues execution at the condition evaluation. - -### Other Structures - -**Require:** -Dynamically includes and evaluates a specified PHP file but cannot include other Zephir files at runtime. - -```zephir -require "path/to/file.php"; -``` - -**Let Statement:** -Used to mutate variables, properties and arrays since variables are by default immutable. Supports increment/decrement operations and multiple simultaneous assignments. - -```zephir -let a = 10; -let a++, b--; -let c = a + b; -``` - ---- - -## Arrays - -### Declaration - -Variables can be declared as arrays using `var` (mutable type) or `array` (fixed type). When using the `array` keyword, the variable's type cannot be changed across execution. - -```zephir -var dynamicArray; -array fixedArray; -``` - -### Creation Methods - -Arrays are constructed using square brackets with various structures: - -```zephir -// Empty arrays -let arr = []; - -// Indexed arrays -let arr = [1, 2, 3, 4]; - -// Associative arrays -let arr = ["foo": "bar", "baz": "qux"]; - -// Mixed key types -let arr = [0: "first", "key": "second", 1: "third"]; - -// Nested/multi-dimensional structures -let arr = [[1, 2], [3, 4]]; -``` - -### Updating Elements - -Modification occurs through bracket notation with specific keys: - -```zephir -// String-keyed access -let elements["foo"] = "bar"; - -// Numeric-keyed access -let elements[0] = "bar"; - -// Nested updates -let elements[0]["foo"] = "bar"; -``` - -### Appending Elements - -New items are added to the end using empty brackets: - -```zephir -let elements[] = "bar"; -``` - -### Retrieval - -Elements are accessed identically to updates, using either string or numeric keys with bracket notation to fetch stored values. - -**Key Concept**: Zephir arrays function as hash tables, enabling flexible key-value storage similar to PHP's array implementation, supporting both indexed and associative structures within the same collection. - ---- - -## Functions - -### Function Calls - -**Built-in PHP Functions:** -Zephir enables direct invocation of PHP functions: - -```zephir -if strlen(text) != 0 { - let encoded = base64_encode(text); -} -``` - -**Custom Functions:** -The language supports calling user-defined PHP functions. Verify existence using `function_exists()` before invocation to prevent errors: - -```zephir -if function_exists("my_custom_encoder") { - let encoded = my_custom_encoder(text); -} -``` - -### Parameter Handling - -**Dynamic Variables:** -Functions accept dynamic-typed parameters directly. When passing statically-typed arguments, Zephir automatically generates temporary dynamic variables for the function call. - -**Return Value Assignment:** -Dynamic return values require explicit casting when assigned to static variables: - -```zephir -let encoded = (string) base64_encode(text); -``` - -### Advanced Function Calls - -**Dynamic Function Invocation:** -Zephir supports calling functions stored in variables using the syntax `{callback}(text)`, where the callback parameter holds a function reference. - -```zephir -public function process(var callback, string text) -> string -{ - return {callback}(text); -} -``` - ---- - -## Object-Oriented Programming - -### Class Structure - -Zephir enforces OOP principles by requiring that **every Zephir file must implement a class or an interface (and just one).** - -```zephir -namespace PHPStanTurbo; - -class MyClass -{ - // class content -} -``` - -### Class Modifiers - -Two modifiers control class behavior: -- **final**: Prevents extension by subclasses -- **abstract**: Prevents direct instantiation - -```zephir -final class CannotExtend -{ -} - -abstract class MustExtend -{ -} -``` - -### Interface Implementation - -Classes can implement multiple interfaces. When using interfaces from external extensions (like PSR), developers must create stub interfaces that extend the external ones. - -**Important**: It is the developer's responsibility to ensure that all external references are present before the extension is loaded. - -```zephir -namespace PHPStanTurbo; - -class MyClass implements \Countable, \ArrayAccess -{ -} -``` - -### Methods - -Methods require explicit visibility declarations (public, protected, or private). - -**Parameters:** -Methods support required and optional parameters with default values and type hints. Parameters can be marked as `const` for read-only access. - -```zephir -public function process(string text, int limit = 100) -> string -{ - return text; -} - -public function readOnly(const array data) -> int -{ - // data cannot be modified - return count(data); -} -``` - -**Modifiers:** -- `static`: Class-level methods -- `final`: Cannot be overridden -- `deprecated`: Marks methods as deprecated - -```zephir -public static function combinations(array arrays) -> array -{ - // static method -} - -final public function cannotOverride() -> void -{ -} - -deprecated public function oldMethod() -> void -{ -} -``` - -**Return Types:** -Methods support return type hints, including multiple types separated by `|`, and `void` declarations. - -```zephir -public function getValue() -> string|int -{ - return "value"; -} - -public function process() -> void -{ - // no return value -} -``` - -**Visibility:** -- Public methods export to the PHP extension -- Protected methods are accessible to inheriting classes -- Private methods remain internal to the class - -### Properties - -Properties require visibility modifiers and can have compile-time default values. They're accessed using the `->` operator. - -```zephir -class MyClass -{ - private string name = "default"; - protected int counter = 0; - public array items = []; - - public function getName() -> string - { - return this->name; - } -} -``` - -**Dynamic Property Access:** -Use bracket notation for dynamic property names: - -```zephir -let value = this->{"propertyName"}; -``` - -### Constants - -Classes support immutable constants declared with `const`, accessed via the static operator `::`. - -```zephir -class MyClass -{ - const VERSION = "1.0.0"; - const MAX_SIZE = 1024; -} - -// Access -let version = MyClass::VERSION; -``` - -### Advanced Features - -**Getter/Setter Shortcuts:** -Properties can use `get`, `set`, and `toString` shortcuts to auto-generate methods. - -**Named Parameters:** -Methods support keyword arguments for flexible parameter passing. - ---- - -## Closures (Anonymous Functions) - -Zephir supports closures, which are PHP-compatible anonymous functions that can be seamlessly utilized in Zephir and returned to PHP userland code. - -### Key Characteristics - -**Execution and Parameters:** -Closures can be executed directly within Zephir and passed as parameters to other functions or methods. They accept input parameters and return computed values. - -### Three Syntax Forms - -**1. Standard Syntax:** -Full closure declaration with explicit parameter and return statement: - -```zephir -let closure = function(int number) { - return number * 2; -}; -``` - -**2. Direct Execution:** -Closures applied to array methods, transforming each element: - -```zephir -let results = array->map(function(item) { - return item * item; -}); -``` - -**3. Arrow Syntax:** -A concise shorthand using the `=>` operator: - -```zephir -let squared = array->map(number => number * number); -``` - -The arrow function syntax provides a more compact way to express closures, enhancing code readability. - ---- - -## Exception Handling - -Zephir implements exceptions at a low level with functionality similar to PHP, enabling developers to throw and catch exceptions within try-catch blocks. - -### Throwing Exceptions - -**Basic Exception Throwing:** -```zephir -throw new \Exception("This is an exception"); -``` - -**Literal Throwing:** -Zephir allows throwing literals or typed variables directly as exception messages: - -```zephir -throw "Test"; -throw 123; -throw 123.123; -``` - -### Catching Exceptions - -**Standard Catch Blocks:** -```zephir -try { - // code that might throw -} catch \Exception, e { - echo e->getMessage(); -} -``` - -**Silent Try Blocks:** -A "silent" try block ignores exceptions without capturing them, requiring no catch clause: - -```zephir -try { - // code that might throw -} -``` - -**Optional Exception Variable:** -The exception variable in a catch clause is optional if not needed: - -```zephir -catch \Exception { - // handle without accessing exception -} -``` - -**Multiple Exception Types:** -Single catch blocks can handle multiple exception types using pipe syntax: - -```zephir -catch \RuntimeException|\Exception, e { - // handle multiple types -} -``` - -### Exception Information - -Zephir exceptions provide methods consistent with PHP exceptions: -- `getMessage()`: Get exception message -- `getFile()`: Get file where exception was thrown -- `getLine()`: Get line number where exception was thrown - ---- - -## Built-In Methods - -Zephir provides object-oriented methods for various data types that correspond to procedural equivalents. **Important**: Calling methods on static-typed variables has no impact on performance, as Zephir internally transforms the code from the object-oriented version to the procedural version. - -### String Methods - -- `format()` / `sprintf()` - formatting strings -- `index()` / `strpos()` - locating substrings -- `length()` / `strlen()` - measuring string size -- `lower()` / `strtolower()` - converting to lowercase -- `lowerfirst()` / `lcfirst()` - lowercasing initial character -- `md5()` and `sha1()` - cryptographic hashing -- `trim()`, `trimleft()`, `trimright()` - whitespace removal -- `upper()` / `strtoupper()` - uppercase conversion -- `upperfirst()` / `ucfirst()` - capitalizing first character - -```zephir -string text = "hello world"; -let text = text.upper(); // "HELLO WORLD" -let len = text.length(); // 11 -``` - -### Array Methods - -- `combine()`, `merge()` - combining arrays -- `diff()`, `intersect()` - comparing arrays -- `flip()` - swapping keys and values -- `hasKey()` - existence checking -- `join()` - concatenating elements -- `keys()`, `values()` - extracting components -- `pad()` - extending arrays -- `rev()`, `reversed()` - reversing order -- `split()` - partitioning into chunks -- `walk()` - applying functions to elements - -```zephir -array items = [1, 2, 3]; -let reversed = items.reversed(); // [3, 2, 1] -if items.hasKey(0) { - // key exists -} -``` - -### Other Data Types - -- **Char**: `toHex()` - hexadecimal conversion -- **Integer**: `abs()` - absolute value calculation - ---- - -## Configuration (config.json) - -Zephir extensions use a `config.json` file to control build and compiler behavior. - -### Core Settings - -```json -{ - "namespace": "phpstanturbo", - "name": "phpstan_turbo", - "author": "PHPStan Team", - "version": "0.0.1", - "description": "PHPStan Turbo Extension", - "extension-name": "phpstanturbo" -} -``` - -- **namespace**: Extension namespace (regex: `[a-zA-Z0-9\_]+`) -- **name**: Extension name in C code (ASCII characters only) -- **author**: Developer/organization information -- **version**: Extension version (format: `[0-9]+\.[0-9]+\.[0-9]+`) -- **description**: Extension description text -- **extension-name**: Base filename (defaults to namespace value) - -### Documentation & Output - -**API Documentation:** -Controls HTML documentation generation: - -```json -{ - "api": { - "path": "doc/%version%", - "theme": { - "name": "zephir", - "options": { - "github": null, - "analytics": null, - "main_color": "#3E6496" - } - } - } -} -``` - -**Stubs:** -Configures IDE documentation stub generation: - -```json -{ - "stubs": { - "path": "ide/%version%/%namespace%/", - "stubs-run-after-generate": false - } -} -``` - -**Info:** -Defines phpinfo() section structure with headers and data rows: - -```json -{ - "info": [ - { - "header": ["Directive", "Value"], - "rows": [ - ["setting1", "value1"], - ["setting2", "value2"] - ] - } - ] -} -``` - -### Compilation & Dependencies - -**Backend:** -Selects Zend Engine version (ZendEngine2 or ZendEngine3): - -```json -{ - "backend": "ZendEngine3" -} -``` - -**Extra Compilation Options:** -```json -{ - "extra-cflags": "-O2 -Wall", - "extra-libs": "-lm", - "extra-sources": ["utils/pi.c"], - "extra-classes": ["MyPrecompiledClass"], - "constants-sources": ["kernel/math_constants.h"] -} -``` - -**Dependencies:** -```json -{ - "package-dependencies": { - "openssl": ">=1.0.0" - }, - "requires": { - "extensions": ["json", "pdo"] - } -} -``` - -### Code Optimization - -Enable/disable specific compiler optimizations: - -```json -{ - "optimizations": { - "static-type-inference": true, - "static-type-inference-second-pass": true, - "local-context-pass": true, - "constant-folding": true, - "static-constant-class-folding": true, - "call-gatherer-pass": true, - "check-invalid-reads": false, - "internal-call-transformation": false - } -} -``` - -#### Available Optimizations - -1. **call-gatherer-pass**: Counts how many times a function or method is called within the same method, enabling inline caches to speed up repeated calls. - -2. **check-invalid-reads**: Validates that variables are properly initialized during compilation, preventing potential bugs and memory leaks. - -3. **constant-folding**: Simplifies constant expressions at build time, replacing complex calculations with their computed results. - -4. **internal-call-transformation**: Generates internal method implementations alongside PHP ones, bypassing PHP userspace for faster internal calls (off by default). - -5. **local-context-pass**: Transfers heap-allocated variables to the stack, reducing memory indirections during program execution. - -6. **static-constant-class-folding**: Replaces class constant references with their literal values at compile time. - -7. **static-type-inference**: Identifies dynamic variables suitable for conversion to static/primitive types, enabling better C compiler optimization. - -8. **static-type-inference-second-pass**: Conducts a secondary type inference pass leveraging data from the first pass for improved results. - -### Warnings - -Control compiler warning generation: - -```json -{ - "warnings": { - "unused-variable": true, - "unused-variable-external": false, - "possible-wrong-parameter": true, - "possible-wrong-parameter-undefined": false, - "nonexistent-function": true, - "nonexistent-class": true, - "non-valid-isset": true, - "non-array-update": true, - "non-valid-objectupdate": true, - "non-valid-fetch": true, - "invalid-array-index": true, - "non-array-append": true, - "invalid-return-type": true, - "unreachable-code": true, - "nonexistent-constant": true, - "not-supported-magic-constant": true, - "non-valid-decrement": true, - "non-valid-increment": true, - "non-valid-clone": true, - "non-valid-new": true, - "non-array-access": true, - "invalid-reference": true, - "invalid-typeof-comparison": true, - "conditional-initialization": true - } -} -``` - -#### Available Warnings - -1. **unused-variable**: Raised when a variable is declared but not used within a method (enabled by default) -2. **unused-variable-external**: Detects function parameters that are declared but never referenced -3. **possible-wrong-parameter-undefined**: Triggers when a method receives arguments of incorrect types -4. **nonexistent-function**: Raised when a function is called that does not exist at compile time -5. **nonexistent-class**: Raised when a class is used that does not exist at compile time -6. **non-valid-isset**: Identifies isset operations performed on non-array or non-object values -7. **non-array-update**: Detects attempts to update array indices on variables that aren't arrays -8. **non-valid-objectupdate**: Raised when an object update operation is made on a non-object value -9. **non-valid-fetch**: Identifies fetch operations on non-array or non-object variables -10. **invalid-array-index**: Triggers when array indexing uses invalid index types -11. **non-array-append**: Detects append operations on non-array variables - -### Extension Behavior - -**Globals:** -Extension-wide variables with types and defaults: - -```json -{ - "globals": { - "debug": { - "type": "bool", - "default": false, - "module": true - } - } -} -``` - -**Lifecycle Hooks:** - -```json -{ - "initializers": { - "globals": [{"include": "header.h", "code": "init_globals();"}], - "module": [{"include": "header.h", "code": "init_module();"}], - "request": [{"include": "header.h", "code": "init_request();"}] - }, - "destructors": { - "request": [{"include": "header.h", "code": "cleanup_request();"}], - "post-request": [{"include": "header.h", "code": "cleanup_post_request();"}], - "module": [{"include": "header.h", "code": "cleanup_module();"}], - "globals": [{"include": "header.h", "code": "cleanup_globals();"}] - } -} -``` - -### Miscellaneous - -```json -{ - "silent": false, - "verbose": true, - "optimizer-dirs": ["optimizers"], - "prototype-dir": "prototypes" -} -``` - -- **silent**: Suppresses command output -- **verbose**: Enables detailed error messages -- **optimizer-dirs**: Custom optimizer file locations -- **prototype-dir**: Prototype files for required extensions - ---- - -## Extension Lifecycle - -### Initialization Hooks - -The `initializers` block manages three setup phases: - -- **Globals**: Setting up the global variable space -- **Module**: Prepares functionality the extension requires to operate -- **Request**: Readies the extension to handle individual requests - -Each hook is configured in `config.json` as an array containing include/code pairs, where the include references a C header file and code specifies the logic to execute. - -### Shutdown Hooks - -The `destructors` block handles four teardown phases, executing in reverse order: - -- **Request**: Finalizes data before responding to the client -- **Post-request**: Performs cleanup after the response is transmitted -- **Module**: Releases extension resources before PHP process termination -- **Globals**: Cleans up the global variable space - -### Best Practice - -The documentation recommends placing logic longer than a few lines into separate C source files and calling them with single-line function calls within the `code` value, rather than embedding complex logic directly in the configuration. - ---- - -## Extension Globals - -Extension globals enable developers to establish and manage global variables within an extension. - -### Key Characteristics - -Extension globals support only simple scalar types like `int`, `bool`, `double`, and `char`. They function as configuration options that can alter library behavior. - -### Configuration Setup - -Add a `globals` structure to your `config.json` file. Each global requires a type and default value. Optional namespacing uses dot notation (e.g., `component.setting`). - -The `module` key places the global's initialization process into the module-wide `GINIT` lifecycle event. This ensures the global is set up once per process rather than per request. - -### Access Methods - -Within Zephir code, use built-in functions `globals_get()` and `globals_set()` to read and modify extension globals: - -```zephir -public function isDebugEnabled() -> bool -{ - return globals_get("debug"); -} - -public function enableDebug() -> void -{ - globals_set("debug", true); -} -``` - -For PHP-level access, create wrapper methods that call these functions. - -### Important Limitation - -Extension globals cannot use dynamic variable names. The C code generated by the `globals_get`/`globals_set` optimizers must be resolved at compilation time, meaning variable names must be hardcoded—not computed or fetched from other variables. - ---- - -## Static Analysis Features - -Zephir's compiler includes static analysis capabilities designed to identify potential issues before runtime execution. - -### Conditional Unassigned Variables - -The compiler detects when variables might be used before assignment. When such cases are found, Zephir automatically initializes the variable and generates a warning message. This proactive approach helps developers catch logic errors that could lead to unexpected behavior. - -### Dead Code Elimination - -Zephir identifies unreachable code branches and removes them from the compiled binary. The tool automatically eliminates code that cannot be executed, such as statements within conditional blocks that always evaluate to false, streamlining the final output. - ---- - -## Custom Optimizers - -Optimizers intercept function calls during compilation, replacing PHP userland functions with direct C calls for improved performance and reduced overhead. - -### Structure and Naming Convention - -Optimizers are PHP classes stored in a configurable directory. They follow a specific naming pattern: -- Zephir function `calculate_pi` → Optimizer class `CalculatePiOptimizer` -- File location: `optimizers/CalculatePiOptimizer.php` -- Corresponding C function: `my_calculate_pi` - -### Key Implementation Steps - -**1. Validate Parameters:** -The optimizer must verify parameter count and type requirements, throwing `CompilerException` if validation fails. - -**2. Process Return Values:** -Check if the value returned will be stored in the correct type; if not, throw a compiler exception. - -**3. Resolve Parameters:** -Use `getReadOnlyResolvedParams()` for read-only functions or `getResolvedParams()` when parameters are modified. This returns valid C code for the code printer. - -**4. Return CompiledExpression:** -All optimizers must return a `CompiledExpression` instance specifying the return type and generated C code. - -### Configuration - -Register optimizers in `config.json`: - -```json -{ - "optimizer-dirs": ["optimizers"], - "extra-sources": ["utils/pi.c"] -} -``` - -The actual C implementation must reside in the `ext/` directory and include necessary Zend Engine headers. - ---- - -## Zephir CLI Commands - -### Available Commands - -**zephir init** -Creates new Zephir extension project: -```bash -zephir init namespace [--backend=ZendEngine3] -``` - -**zephir generate** -Converts Zephir code to C: -```bash -zephir generate [--backend=ZendEngine3] -``` - -**zephir compile** -Builds a Zephir extension: -```bash -zephir compile [--backend=ZendEngine3] [--dev|--no-dev] -``` -- `--dev`: Enables development mode with debug symbols -- `--no-dev`: Creates production-ready extension - -**zephir build** -Meta command executing generate, compile, and install sequentially: -```bash -zephir build -``` - -**zephir install** -Deploys extension to system directory: -```bash -zephir install [--dev|--no-dev] -``` - -**zephir api** -Generates HTML API documentation: -```bash -zephir api [--path=PATH] [--output=OUTPUT] [--url=URL] [--options=OPTIONS] -``` - -**zephir stubs** -Creates PHP IDE stub files: -```bash -zephir stubs [--backend=ZendEngine3] -``` - -**zephir clean** -Removes object files from the extension: -```bash -zephir clean -``` - -**zephir fullclean** -Removes all object files including phpize-generated files: -```bash -zephir fullclean -``` - -**zephir help** -Shows command documentation: -```bash -zephir help -``` - -**zephir list** -Displays all available commands: -```bash -zephir list -``` - ---- - -## PHPInfo() Integration - -### Automatic Functionality - -Zephir extensions automatically display basic information in phpinfo() output, including the extension version and any INI options the extension supports. - -### Custom Configuration - -Developers can expand phpinfo() output by adding a configuration section to `config.json`. This allows creation of custom information tables with defined headers and row data. - -The configuration uses a JSON array format where each object represents a separate table. Each table specifies: -- **header**: Column titles (e.g., "Directive" and "Value") -- **rows**: Data entries organized as key-value pairs - -Custom directives configured this way appear as formatted tables within the phpinfo() output, making extension-specific settings and environment data easily accessible to developers during debugging and deployment. - ---- - -# PHPStan Turbo Extension Development - -## Current Implementation - -### CombinationsHelper.zep - -**Location**: `turbo-ext/phpstanturbo/CombinationsHelper.zep` - -**Purpose**: Generates Cartesian product of arrays (all possible combinations) - -**Signature**: -```zephir -public static function combinations(array arrays) -> array -``` - -**Algorithm**: -- Recursive implementation -- Base case: empty input returns `[[]]` -- Recursive case: Takes first array (head), processes remaining arrays -- Builds combinations by prepending each element from head to sub-combinations - -**Implementation**: -```zephir -namespace PHPStanTurbo; - -final class CombinationsHelper -{ - /** - * @param array arrays - * @return array - */ - public static function combinations(array arrays) -> array - { - var head, elem, combination, c, comb, subResult, results; - array remaining; - - if count(arrays) === 0 { - return [[]]; - } - - let remaining = arrays; - let head = array_shift(remaining); - let results = []; - - for elem in head { - let subResult = self::combinations(remaining); - for combination in subResult { - let comb = [elem]; - for c in combination { - let comb[] = c; - } - let results[] = comb; - } - } - - return results; - } -} -``` - -**Key Difference from PHP Version**: -- PHP version uses generators (`yield`) for memory efficiency -- Zephir version returns full array (generators not yet implemented in Zephir) -- This trade-off may impact memory usage for large combination sets - -## Zephir Configuration (`config.json`) - -Our current configuration for the PHPStan Turbo Extension: - -```json -{ - "stubs": { - "path": "ide/%version%/%namespace%/", - "stubs-run-after-generate": false, - "banner": "" - }, - "api": { - "path": "doc/%version%", - "theme": { - "name": "zephir", - "options": { - "github": null, - "analytics": null, - "main_color": "#3E6496", - "link_color": "#3E6496", - "link_hover_color": "#5F9AE7" - } - } - }, - "warnings": { - "unused-variable": true, - "unused-variable-external": false, - "possible-wrong-parameter": true, - "possible-wrong-parameter-undefined": false, - "nonexistent-function": true, - "nonexistent-class": true, - "non-valid-isset": true, - "non-array-update": true, - "non-valid-objectupdate": true, - "non-valid-fetch": true, - "invalid-array-index": true, - "non-array-append": true, - "invalid-return-type": true, - "unreachable-code": true, - "nonexistent-constant": true, - "not-supported-magic-constant": true, - "non-valid-decrement": true, - "non-valid-increment": true, - "non-valid-clone": true, - "non-valid-new": true, - "non-array-access": true, - "invalid-reference": true, - "invalid-typeof-comparison": true, - "conditional-initialization": true - }, - "optimizations": { - "static-type-inference": true, - "static-type-inference-second-pass": true, - "local-context-pass": true, - "constant-folding": true, - "static-constant-class-folding": true, - "call-gatherer-pass": true, - "check-invalid-reads": false, - "internal-call-transformation": false - }, - "extra": { - "indent": "spaces", - "export-classes": false - }, - "namespace": "phpstanturbo", - "name": "phpstan_turbo", - "description": "", - "author": "PHPStan", - "version": "0.0.1", - "verbose": false, - "requires": { - "extensions": [] - } -} -``` - -Key configuration points: -- **Namespace**: `phpstanturbo` (maps to PHP namespace `PHPStanTurbo`) -- **Extension Name**: `phpstan_turbo` -- **Version**: `0.0.1` -- **Optimizations**: Enabled static type inference, constant folding, call gatherer pass -- **Warnings**: Comprehensive warning set enabled for code quality - -## Development Workflow - -### Building the Extension - -```bash -cd turbo-ext -vendor/bin/zephir generate && vendor/bin/zephir compile -``` - -This generates: -- C source code in `ext/` -- Compiled `phpstanturbo.so` in `ext/modules/` - -### Enabling the Extension - -Add to your `php.ini`: -```ini -extension=/absolute/path/to/phpstan-src/turbo-ext/ext/modules/phpstanturbo.so -``` - -Verify loading: -```bash -php -m | grep phpstanturbo -``` - -### Testing Integration - -1. With extension enabled, run PHPStan: - ```bash - bin/phpstan analyse ... - ``` - -2. Verify extension is being used by checking that `TurboExtensionEnabler::isLoaded()` returns true - -3. Run PHPStan's test suite: - ```bash - vendor/bin/phpunit - ``` - -## Integration Points - -### Files That Use CombinationsHelper - -1. **src/Type/Constant/ConstantArrayType.php:1829** - - Heavy usage during constant array type inference - - Processes combinations of possible types - -2. **src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php** - - Analyzes sprintf format string possibilities - -3. **src/Type/Php/ImplodeFunctionReturnTypeExtension.php** - - Analyzes implode array possibilities - -### Files in Turbo Infrastructure - -1. **src/Turbo/TurboExtensionEnabler.php** - - Extension detection and activation - - Class aliasing logic - -2. **src/Internal/CombinationsHelper.php** - - Fallback PHP implementation - - Gets replaced when extension is loaded - -3. **bin/phpstan** - - Early initialization of turbo extension - -4. **tests/bootstrap.php** - - Ensures tests use extension when available - -5. **src/DependencyInjection/Configurator.php** - - Uses TurboExtensionEnabler - -## Performance Considerations - -### Expected Benefits -- Reduced CPU time for combination generation -- Lower overhead from PHP VM interpretation -- Potential for better memory locality in native code - -### Current Limitations -- No generator support in Zephir version (memory trade-off) -- Only one function optimized so far -- Extension build/deployment complexity - -### Benchmarking -TODO: Add benchmark suite to measure actual performance gains - -## Future Expansion Areas - -### High-Priority Candidates -Functions that are: -- Called frequently during analysis -- Computationally intensive -- Work with primitive data structures -- Don't require complex PHP ecosystem features - -### Potential Targets - -1. **Array/String Operations** - - Array intersection/union operations - - String manipulation utilities - - Hash computations - -2. **Type System Operations** - - Type comparison/equality checks - - Simple type transformations - - Type acceptability checks - -3. **Scope/Variable Tracking** - - Scope merging operations - - Variable state tracking - -### Investigation Workflow - -To identify new optimization targets: - -1. **Profile PHPStan**: Find hotspots using Xdebug or Blackfire -2. **Identify Pure Functions**: Focus on methods without side effects -3. **Assess Complexity**: Ensure Zephir can express the logic -4. **Prototype**: Implement in Zephir -5. **Benchmark**: Measure actual performance impact -6. **Integrate**: Add to TurboExtensionEnabler aliasing - -## Development Best Practices - -### Adding New Zephir Classes - -1. Create `.zep` file in `turbo-ext/phpstanturbo/` -2. Use namespace `PHPStanTurbo` -3. Match the interface of the PHP class being replaced -4. Update `TurboExtensionEnabler.php` with new class_alias -5. Keep fallback PHP implementation in sync - -### Zephir Coding Guidelines - -**Variable Declarations:** -- Always declare all variables at the beginning of methods -- Use static types when possible for optimization -- Use `var` for dynamic types when necessary - -**Type Safety:** -- Specify return types on all methods -- Use type hints for parameters -- Be aware of automatic type conversions -- **Remember**: String indexing (`str[i]`) returns `char`, not `string` -- Use single quotes for `char` literals (`'a'`), double quotes for `string` literals (`"a"`) - -**Memory Efficiency:** -- Prefer static types over dynamic for performance -- Be mindful that arrays are not generators (full materialization) -- Use `const` parameters when values shouldn't be modified - -**Error Handling:** -- Use exceptions for error conditions -- Validate parameters early -- Provide clear error messages - -### Testing Strategy - -1. Ensure PHP implementation has comprehensive tests -2. Extension should pass same test suite -3. Add specific tests for edge cases in Zephir -4. Test both with and without extension loaded - -### Compatibility - -- Maintain API compatibility with PHP version -- Document any behavioral differences (e.g., generators vs arrays) -- Ensure graceful fallback when extension not available - -## Debugging - -### Extension Not Loading - -```bash -# Check if extension file exists -ls -l turbo-ext/ext/modules/phpstanturbo.so - -# Check PHP configuration -php --ini - -# Check for loading errors -php -d extension=/path/to/phpstanturbo.so -m - -# Verify extension info -php --re phpstanturbo -``` - -### Rebuild from Scratch +# turbo-ext — instructions for working on the native extension + +Read `README.md` first: the stub-shadowing pattern, the sync machinery, and +the seven **Design rules for new ports** there are binding. This file is the +operational checklist on top of them. + +## Before porting anything: estimate, then decide + +Ports pay off by absorbing *call frames*, priced at roughly 40ns per absorbed +userland frame. Count calls first (SPX: `SPX_ENABLED=1` on a self-analysis +run) and multiply — a site called 400× per run can never pay; a site absorbing +millions of tiny calls can. Three finished, correct, all-tests-green tier-1 +ports were reverted because they measured ≈0% — being correct is not the bar, +being ≥0.5% faster is. When the estimate is marginal, don't port. + +## Shadowing new code — do the steps in this order + +1. **Extract the PHP code** into a dedicated class (static methods are fine) + under `src/`, called unconditionally from the original sites — no + turbo-conditional branches in callers. Find **all** call sites (beware: + `grep "\$this->foo"` in double quotes sends `\$` to grep and silently + matches nothing — use single quotes). Run the full test suite now, before + any native work. +2. **Check it is not a DI service** (rule 6 in README — this is fatal and + was measured at 617 broken tests). Value classes and manually-instantiated + classes are safe. +3. **Implement natively**: one class per `.cpp` in `src/`, namespace + `PHPStanTurbo`, class **non-final**, `instanceof`-style checks instead of + exact class-entry comparisons. Hot classes are registered with the raw + Zend API in `main.cpp`'s `onStartup` (never PHP-CPP trampolines — + `Php::Parameters` allocates per call). Reuse the `pt_*` helpers in + `support.h`/`support.cpp` before writing new ones. + + **Style**: the logic lives in a C++ handle class in `namespace + phpstan_turbo` that mirrors the PHP twin method for method (see + `TrinaryLogic.cpp` as the reference; `and`/`or` keyword clashes get a + trailing underscore); registration goes through the `reg::Class` builder + in `reg.h` — one `cls.method("name", flags, requiredArgs, { args... }, + lambda)` declaration per method, where the lambda body is only + ZEND_PARSE_PARAMETERS glue + one delegation line (see TrinaryLogic.cpp). + Never register hot classes via PHP-CPP's `Php::Class` — its trampoline + boxes every argument per call. Use the zero-cost + wrappers in `zv.h` — borrowed `zv::Ref` views vs owned move-only + `zv::Val` RAII values (UNDEF `Val` = pending exception), `zv::ArrRef` + range-for instead of hand-rolled `ZEND_HASH_FOREACH` (it handles the + packed layout of PHP 8.2+ — never walk Buckets by hand). Zero-cost is + the bar: no virtuals, no exceptions, no allocations the raw form would + not make; where an abstraction is not provably free, keep the raw zend + form and say so in a comment. New generic helpers go into `zv.h` + following its conventions, never as one-offs. +4. **Class names the native code needs** go through + `Runtime::configure()` as `::class` constants in `TurboExtensionEnabler` + (scoped-phar safety). Classes the native code *instantiates* get `…Impl` + entries naming the stub, so instances satisfy the original type hints. +5. **Add the stub** `stubs/Foo.php` (`final class Foo extends + \PHPStanTurbo\Foo {}`) and its `require_once` in `TurboExtensionEnabler`. +6. **Regenerate the manifest**: `php bin/side-by-side.php --update-manifest` + (derives `shadowed-classes.json` from `stubs/` + the autoloader — never + edit it by hand), then run `php bin/side-by-side.php --check` — method + parity must pass. +7. **Extend `tests/smoke.php`** with differential coverage (native result + must equal the PHP implementation's result on the same inputs). +8. **Verify**: strict build, smoke test, + `php -d extension=$PWD/turbo-ext/phpstan_turbo.so turbo-ext/tests/signature-parity.php` + (arginfo parameter names must match the PHP twin exactly — named arguments), + full `make tests` with the extension loaded, and byte-identical analysis + output with the extension on vs `PHPSTAN_TURBO=0`. Anything touching + `src/parser/` additionally runs `turbo-ext/tests/parser-corpus.php` + (byte-identical ASTs over the whole corpus); a php-parser version bump in + composer.lock requires the same. +9. **Benchmark** (protocol below). ≤0.5% → revert the port, keep the PHP + extraction only if it stands on its own. +10. **Version bump**: commit the change, then a follow-up commit setting + `TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION` to + `git log -1 --format=%H -- turbo-ext/src | cut -c1-7`. + The binary's own version is baked from git at build time, so only the + PHP constant is hand-edited. The bump cannot be part of the same commit — + the SHA would change under it. + +## Build and verify commands ```bash cd turbo-ext -rm -rf ext/ .zephir/ -vendor/bin/zephir generate && vendor/bin/zephir compile -``` - -### Zephir Compilation Errors - -- Check Zephir syntax against [Zephir Language Reference](https://docs.zephir-lang.com/) -- Ensure C compiler and PHP dev headers are available -- Review generated C code in `ext/` directory -- Check that all variables are declared before use -- Verify type compatibility in assignments - -### Common Pitfalls - -1. **Undeclared Variables**: All variables must be declared with `var` or a type -2. **Type Mismatches**: Static types cannot be reassigned to different types -3. **String vs Char Confusion**: - - String indexing returns `char`, not `string`: `str[0]` is char type - - Use single quotes for char: `'a'`, double quotes for string: `"a"` - - When iterating strings, loop variable is `char` type - - Comparing `str[0] == "a"` is wrong; use `str[0] == 'a'` instead - - **PHP to Zephir**: Convert all single-quoted PHP strings to double-quoted Zephir strings -4. **Missing Semicolons**: While optional, they can help avoid ambiguity -5. **File/Class Mismatch**: File structure must match namespace and class names -6. **Immutable Variables**: Use `let` to assign values, not direct assignment - -## Resources - -### Documentation -- [Zephir Official Documentation](https://docs.zephir-lang.com/) -- [Zephir GitHub Repository](https://github.com/zephir-lang/zephir) -- [Zephir Tutorial](https://docs.zephir-lang.com/latest/tutorial/) -- [PHP Extension Development](https://www.phpinternalsbook.com/) - -### Tools -- [Zephir Parser](https://github.com/zephir-lang/php-zephir-parser) -- [Zephir Compiler](https://github.com/zephir-lang/zephir) - -### Community -- [Zephir Documentation Repository](https://github.com/zephir-lang/documentation) - -## Contributing to Turbo Extension - -When adding new optimizations: - -1. **Measure First**: Profile to confirm bottleneck -2. **Start Simple**: Pick pure functions with simple data types -3. **Maintain Compatibility**: Keep same interface as PHP version -4. **Test Thoroughly**: Run full test suite with/without extension -5. **Document Trade-offs**: Note any behavioral differences -6. **Benchmark Results**: Provide performance measurements - -## Known Issues & Limitations - -1. **Generator Support**: Zephir version uses arrays instead of generators - - Impact: Higher memory usage for large combination sets - - Mitigation: Monitor memory usage in production - -2. **Build Complexity**: Requires Zephir toolchain and C compiler - - Impact: Development setup more complex - - Mitigation: Clear documentation and prerequisites - -3. **Distribution**: Extension must be compiled per environment - - Impact: Cannot distribute as pure PHP package - - Mitigation: Keep extension optional with fallback - -4. **Debugging**: Native code harder to debug than PHP - - Impact: Longer development cycles - - Mitigation: Comprehensive PHP tests before porting - -## Version History - -- **0.0.1** (Current): Initial PoC with CombinationsHelper only - -## Contact & Support - -For issues or questions: -- Main PHPStan repo: https://github.com/phpstan/phpstan -- Zephir issues: https://github.com/zephir-lang/zephir/issues - ---- - -**Last Updated**: 2025-12-30 -**Maintainers**: PHPStan Team +make WARN_FLAGS="-Wall -Wextra -Werror -Wno-assume -Wno-unused-parameter -Wno-unicode" +php -d extension=$(pwd)/phpstan_turbo.so tests/smoke.php # must print ALL OK +``` + +The three `-Wno-` exemptions are for zend macro expansions only (documented in +`.github/workflows/turbo-ext.yml`); new warnings in our code are fixed, not +exempted. Use `-I` for PHP-CPP headers, never `-isystem` (Apple clang lets a +stale `/usr/local/include/phpcpp.h` shadow `-isystem` paths); third-party +header noise is handled by the pragma guards in `support.h`. + +## Benchmark protocol + +Judge **user CPU**, never wall clock, on interleaved A/B pairs (the machine +drifts ±1s thermally — never run all A then all B): + +```bash +bin/phpstan clear-result-cache -c build/phpstan.neon -q +/usr/bin/time php -d memory_limit=6G bin/phpstan analyse -c build/phpstan.neon --debug -q src +# baseline runs: prefix with PHPSTAN_TURBO=0 (the ini loads the extension globally) +``` + +Output identity: `--error-format=raw` runs in both modes must diff empty. + +## Zend-level gotchas (each of these cost real debugging time) + +- PHP literal `[]` is the read-only `zend_empty_array` in RODATA: + `ZVAL_ARR`/`RETVAL_ARR` force refcounted flags and a later addref SIGBUSes. + Check `GC_FLAGS(ht) & IS_ARRAY_IMMUTABLE` first (see `pt_*` array helpers). +- Expression-table keys can be numeric strings that PHP coerced to int keys: + always use `zend_symtable_*` / the dual string+index `pt_ht_*` helpers, + never plain `zend_hash_find` on user-derived keys. +- Private userland methods are callable from C via `ce->function_table` + lookup + `zend_call_known_function` — no visibility check applies. +- Globals are plain statics (NTS-only build); keep new state in `pt_globals`. +- Fibers can suspend from internal frames — native code may be re-entered. +- Cloned scopes must reset per-instance memo properties to constructor + defaults, and native factories must instantiate the `…Impl` stub classes. + +## Updating php-parser (the native parser engine) + +Follow README.md's "Updating php-parser" procedure. The agent-relevant traps: + +- The reduce actions are GENERATED — never hand-edit + `ParserRunnerActions{1,2,3}.cpp` or `ParserRunnerActionsSplit.h` (CI + regenerates and diffs). After any `Php8.php` change, run + `php turbo-ext/bin/generate-parser-actions.php`; it fails listing any + closure whose body changed upstream and has no override — port those + (usually by updating the matching + `src/parser/action-overrides/.inc`; overrides are keyed by body + content, so pure renumbering never needs hand work), re-run, then + corpus-verify. +- `ParserEngine::reduce` dispatches via `PN_REDUCE_SPLIT_1/2` from the generated + `ParserRunnerActionsSplit.h`; the generator rebalances the three action + files automatically. +- The corpus differential (`tests/parser-corpus.php`) is the acceptance bar + — byte-identical serialized ASTs, errors, and token streams — and it only + proves what the corpus contains: new syntax needs fixtures in the repo + before the check means anything for it. +- Finish with both pins: `SUPPORTED_PHP_PARSER_VERSION` in + `.github/workflows/turbo-ext.yml` plus the extension version bump in + `TurboExtensionEnabler` (`src/parser/` changed). + +## PHP-side constraints + +- No `match` expressions in `src/` (the PHP downgrade tooling cannot handle + them) — use `switch` or if/else. +- The PHP twin stays the reference implementation: fix behavior there first, + port second, and keep both sides structurally parallel so the side-by-side + view stays reviewable. diff --git a/turbo-ext/Makefile b/turbo-ext/Makefile new file mode 100644 index 00000000000..beb07ea9ba0 --- /dev/null +++ b/turbo-ext/Makefile @@ -0,0 +1,75 @@ +# +# phpstan_turbo — build with a locally built PHP-CPP (statically linked). +# +# make builds phpstan_turbo.so +# make phpcpp builds the bundled PHP-CPP library first +# make clean +# + +PHP_CONFIG ?= php-config +PHPCPP_DIR := PHP-CPP + +CXX ?= c++ +# CI overrides with stricter settings, e.g. WARN_FLAGS="-Wall -Wextra -Werror" +# (third-party headers — PHP-CPP, zend — are exempted via the pragma guard in +# src/support.h; -isystem is not usable here because Apple clang lets the +# default /usr/local/include shadow -isystem paths, picking up a stale +# system-installed PHP-CPP) +WARN_FLAGS ?= -Wall +CXXFLAGS := $(WARN_FLAGS) -O2 -std=c++17 -fPIC \ + -I$(PHPCPP_DIR) \ + `$(PHP_CONFIG) --includes` + +# The extension version is the short SHA of the last commit touching +# turbo-ext/src/ or a shadowed PHP twin (the same watched set the CI version +# job enforces against TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION), +# computed from git at build time and baked in via -D. Outside a git checkout +# it degrades to "dev", which the enabler rejects — the extension then simply +# stays inactive. version.stamp makes a SHA change rebuild main.o. +MAPPED_PHP := $(shell php -n -r 'echo implode(" ", array_map(static fn ($$e) => $$e["php"], array_filter(json_decode(file_get_contents("shadowed-classes.json"), true), static fn ($$e) => !($$e["vendored"] ?? false))));' 2>/dev/null) +PHPSTANTURBO_VERSION := $(shell git -C .. log -1 --format=%H -- turbo-ext/src $(MAPPED_PHP) 2>/dev/null | cut -c1-7) +ifeq ($(PHPSTANTURBO_VERSION),) +PHPSTANTURBO_VERSION := dev +endif +CXXFLAGS += -DPHPSTANTURBO_VERSION='"$(PHPSTANTURBO_VERSION)"' + +# Undefined PHP engine symbols are resolved by the php binary at load time; +# GNU ld allows them in shared objects by default, Darwin needs the flag. +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +LINK_FLAGS := -undefined dynamic_lookup +endif + +SOURCES := $(wildcard src/*.cpp) $(wildcard src/parser/*.cpp) +OBJECTS := $(SOURCES:.cpp=.o) + +PHPCPP_LIB := $(wildcard $(PHPCPP_DIR)/libphpcpp.a.*) + +phpstan_turbo.so: $(OBJECTS) $(PHPCPP_LIB) + $(CXX) `$(PHP_CONFIG) --ldflags` -shared $(LINK_FLAGS) -o $@ $(OBJECTS) $(PHPCPP_LIB) + +src/%.o: src/%.cpp src/support.h src/zv.h src/reg.h + $(CXX) $(CXXFLAGS) -c -o $@ $< + +src/main.o: version.stamp + +version.stamp: FORCE + @echo '$(PHPSTANTURBO_VERSION)' | cmp -s - $@ 2>/dev/null || echo '$(PHPSTANTURBO_VERSION)' > $@ + +FORCE: + +$(filter src/parser/%.o,$(OBJECTS)): src/parser/ParserEngine.h src/zv.h + +src/parser/ParserRunner.o: src/parser/ParserRunnerActionsSplit.h + +# PHP-CPP carries one local patch (patches/php-cpp-base-count-int64.patch, +# needed on LP64 Darwin, a no-op cast on Linux); application is idempotent. +phpcpp: + git -C $(PHPCPP_DIR) apply --reverse --check ../patches/php-cpp-base-count-int64.patch 2>/dev/null || \ + git -C $(PHPCPP_DIR) apply ../patches/php-cpp-base-count-int64.patch + $(MAKE) -C $(PHPCPP_DIR) + +clean: + rm -f $(OBJECTS) phpstan_turbo.so version.stamp + +.PHONY: phpcpp clean FORCE diff --git a/turbo-ext/README.md b/turbo-ext/README.md index 2cdac9910e0..afa60df7fd8 100644 --- a/turbo-ext/README.md +++ b/turbo-ext/README.md @@ -1,32 +1,246 @@ -PHPStan Turbo extension -=========== +# phpstan_turbo — native acceleration extension for PHPStan -**Highly experimental work-in-progress.** +**Experimental.** A C++ extension built with +[PHP-CPP](https://github.com/CopernicaMarketingSoftware/PHP-CPP) that +reimplements PHPStan's hottest code paths natively. It is entirely optional: +PHPStan behaves identically without it, just slower. With the extension +loaded, analysis output is bit-for-bit identical — only faster (~25% on +PHPStan's own single-threaded self-analysis). -This extension can be used to rewrite parts of PHPStan into native PHP extension using the [Zephir language](https://zephir-lang.com/en). +## How it works — the stub-shadowing pattern -Requirements ------------ +Every shadowed piece of PHP code follows the same three steps: -Check out Zephir's [Installation Guide](https://docs.zephir-lang.com/latest/installation/#prerequisites) guide, especially the [Zephir Parser extension](https://github.com/zephir-lang/php-zephir-parser) installation so that your developer environment can compile code written in .zep files. +1. The code is extracted into a dedicated PHP class (plain PHP, this is what + runs when the extension is absent) — e.g. `PHPStan\Analyser\ScopeOps`, + `PHPStan\Analyser\ExprHandlerDispatch`, `PHPStan\Node\NodeScanner`, or an + existing value class like `PHPStan\TrinaryLogic`. +2. The extension implements the same class natively in the `PHPStanTurbo` + namespace (one class per file in `src/`). +3. When the extension is enabled, `PHPStan\Turbo\TurboExtensionEnabler` + `require`s an empty stub from `stubs/` — `final class Foo extends + \PHPStanTurbo\Foo {}` — *before* the Composer autoloader registers. + All PHP code keeps calling the original class name, transparently getting + the native implementation via inheritance. -Compiling the extension ------------ +Because instances must satisfy the original type hints, the native code never +instantiates its own classes directly: `TurboExtensionEnabler` passes the stub +class names (`…Impl` entries) to `PHPStanTurbo\Runtime::configure()`, and +factories/singletons instantiate those subclasses. + +The extension is version-pinned (`TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION`); +a mismatched extension is ignored. `PHPSTAN_TURBO=0` disables it explicitly. + +The version is the short SHA of the last commit that touched `turbo-ext/src/` +or one of the shadowed PHP classes. The binary's (actual) version is baked in +at build time — the Makefile computes it from git over that same watched set +— so only the expected side, `TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION`, +is declared by hand. After changing either side of a shadowed pair, verify the +implementations still match and add a follow-up commit updating the constant +to the short SHA of the changing commit; the `turbo-ext.yml` version job +enforces the SHA and the compile job verifies the built binary reports what +the enabler expects. Builds outside a git checkout bake the version "dev", +which the enabler rejects — the extension then simply stays inactive. + +## Keeping the two implementations in sync + +`shadowed-classes.json` is the manifest of shadowed pairs: each PHP class and +the C++ file implementing it natively. It is not edited by hand: +`php bin/side-by-side.php --update-manifest` regenerates it from ground truth +(each stub in `stubs/` names the shadowed class, the Composer autoloader +locates its PHP implementation, the same-named `.cpp` is the native side). +Run it after a native port is brought on par with the PHP implementation and +the behaviour is verified by tests — then the CI checks below hold the +committed manifest against both sides. It drives three things: + +- **CI method parity** — `php bin/side-by-side.php --check` (part of the + version job) verifies every public method of each PHP class has a + `PHP_METHOD` counterpart in the C++ file and every `PHP_METHOD` corresponds + to a method of the PHP class. Non-public PHP methods may stay PHP-only + (native code inlines them or uses C helpers). It also verifies the manifest + is complete — stubs, the enabler's `require_once` list and the per-class + `.cpp` files must all correspond 1:1 to manifest entries — and that stubs + are empty shells (a member declared in a stub would exist only when the + extension is loaded). +- **CI signature parity** — `php tests/signature-parity.php` (compile job, + needs the built extension) reflects each native class against its PHP twin: + visibility, staticness, parameter names/optionality/by-ref/variadic, and + types. It also verifies each manifest entry points at the file the class + actually lives in (and that the `vendored` flag matches), so a stale + manifest fails instead of silently comparing against the wrong source. Native arginfo may erase types to none/`object` (it cannot bake + class names of phar-prefixed namespaces into the binary, and engine-level + type checks cost per call), but what it does declare must match, and + parameter names must match exactly — a renamed parameter would break named + arguments only in turbo mode. +- **CI version coupling** — the version job watches the manifest's PHP files + in addition to `turbo-ext/src/` (see above), so a PHP-side edit cannot + silently diverge from the native port. The vendored `PhpParser\NodeTraverser` + pair is excluded from the git check; it is pinned by `composer.lock`. +- **Side-by-side review** — `php bin/side-by-side.php` renders + `side-by-side.html` (gitignored), pairing each method's PHP and C++ + implementations next to each other for maintenance review. + +Semantic equivalence is still proven by the differential smoke test and by +running the full test suite with the extension loaded — the manifest checks +guard structure and force the version bump ritual, not behavior. + +## The native parser engine + +`src/parser/` reimplements php-parser 5.8.0's LALR engine and node building +(`PhpParser\ParserAbstract` + the generated `Parser\Php8`), shadowed through +the `PHPStan\Parser\ParserRunner` seam. The parsing tables are read at run +time from the first `Php8` parser object seen — they are generated data, so +nothing is duplicated — and node classes resolve relative to the parser's +namespace, which keeps the scoped phar working. Tokenization stays in PHP's +C tokenizer (one `Lexer::tokenize()` crossing per file); everything after — +the shift/reduce loop, all 482 semantic actions, attribute arrays, node +construction (direct property-slot writes derived from constructor parameter +names; classes with non-trivial constructors call the real PHP constructor), +error recovery, and comment annotation — is native. Non-`Php8` parsers and +non-string inputs fall back to `$parser->parse()`. + +Because the input domain is "all PHP source code", method-level parity is not +enough here: `tests/parser-corpus.php` parses thousands of files with both +implementations and requires byte-identical serialized ASTs, identical +collected errors, and identical token streams. It runs in CI on every build. + +### Updating php-parser + +The CI version job pins the php-parser version the engine was ported against +(`SUPPORTED_PHP_PARSER_VERSION` in `.github/workflows/turbo-ext.yml`), so a +`composer.lock` bump fails CI until the engine is consciously re-verified: + +1. **Diff what is actually ported.** Only two vendored files matter: + `lib/PhpParser/ParserAbstract.php` (engine loop + semantic helpers → + `src/parser/ParserRunner.cpp` + `ParserRunnerHelpers.cpp`) and the reduce + closures in `lib/PhpParser/Parser/Php8.php` (→ + `ParserRunnerActions{1,2,3}.cpp`, generated). The parsing tables need + nothing — they are generated data read at run time from the parser + object. New node classes also need nothing: classes resolve by name and + property plans derive from constructor parameters at run time (only a new + constructor with real logic needs the `PN_NEW_CTOR` treatment — a table + in the generator). +2. **Regenerate the reduce actions.** `ParserRunnerActions{1,2,3}.cpp` and + `ParserRunnerActionsSplit.h` (the `ParserEngine::reduce` dispatch boundaries) are + generated by `turbo-ext/bin/generate-parser-actions.php` from the + closures in the vendored `Php8.php`, so rule renumbering costs nothing. + Run it; it fails loudly listing any closure whose body changed upstream + (or is new) and has no handling: the transpiler covers the formulaic + majority, and hand-ported special cases live in + `src/parser/action-overrides/.inc` — keyed by + content, so unchanged bodies keep matching regardless of their rule + number. Port the flagged bodies (usually by updating the corresponding + override; the generated cases are the cookbook), re-run until clean. + Orphaned override files (their body no longer exists upstream) are + reported as warnings — delete them once their replacement is handled. + Never hand-edit the generated files: CI regenerates and diffs them. +3. **Verify**: strict build, then `php turbo-ext/tests/parser-corpus.php` + until byte-identical over the whole corpus. New PHP syntax is only covered + once fixtures using it exist in the repo — PHPStan's own test data for the + new syntax provides them; make sure they land before or with the bump. + Then the full test suite and `make phpstan` with the extension loaded, + and `tests/parser-bench.php` to confirm the speedup held. +4. **Bump both pins**: `SUPPORTED_PHP_PARSER_VERSION` in the workflow, and — + since `src/parser/` changed — the extension version + (`TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION`) per + the usual ritual. The version gate is also what protects users: a phar + ships a consistent extension/sources/php-parser triple, and a stale + extension build simply deactivates instead of parsing with drifted + semantics. + +The generator itself (`bin/generate-parser-actions.php`) resolves php-parser +constants (`Modifiers::*`, `Stmt\Use_::TYPE_*`, ...) under the Composer +autoloader at generation time, decides per node class between property-slot +writes (`PN_NEW`) and calling the real PHP constructor (`PN_NEW_CTOR`) — +verifying at generation time that slot-write classes have trivial +assignment-only constructors — and fails the build on anything it cannot +prove it handles. A brand-new node class with constructor logic shows up as +such a failure and needs an entry in the generator's class-policy tables. + +## Building ```bash cd turbo-ext -vendor/bin/zephir generate && vendor/bin/zephir compile +git clone https://github.com/CopernicaMarketingSoftware/PHP-CPP.git # if absent +ln -sfn include PHP-CPP/phpcpp +make phpcpp # builds the bundled PHP-CPP (static lib) +make # builds phpstan_turbo.so ``` -Enabling the extension ------------- +PHP-CPP carries one local patch, `patches/php-cpp-base-count-int64.patch` +(on macOS/arm64 `long` matches no `Php::Value` constructor unambiguously). +`make phpcpp` applies it automatically, and the CI workflow applies the same +file — the patch lives in exactly one place. -Once you have compiled the extension, you should have this file: `turbo-ext/ext/modules/phpstanturbo.so`. +## Enabling -Put the absolute path to it to your php.ini like this: +Add to `php.ini` (recommended — parallel worker processes inherit it): +```ini +extension=/absolute/path/to/phpstan-src/turbo-ext/phpstan_turbo.so ``` -extension=/home/john/dev/phpstan-src/turbo-ext/ext/modules/phpstanturbo.so + +## Code style + +The native sources are C++ that mirrors the PHP implementations they replace: +each shadowed class is a handle class in `namespace phpstanturbo` with the +twin's methods (see `src/TrinaryLogic.cpp` for the reference shape), built on +the zero-cost wrappers in `src/zv.h` — borrowed `zv::Ref` views, owned +move-only `zv::Val` RAII values, range-for HashTable iteration. The wrappers +compile to the same instructions as the raw zend macros (verified by +interleaved A/B benchmark), so readability costs nothing. Classes register +through the fluent builder in `src/reg.h` — PHP-CPP's `extension.add()` look, +but emitting the raw zend structures with raw handler pointers, so there is +no per-call trampoline or `Php::Parameters` boxing; each method's name, +flags, signature and parameter-parsing glue live together in one declaration. +Raw zend form remains where an abstraction would not be provably free — +always with a comment saying so. + +## Design rules for new ports + +Measured in the July 2026 benchmarks (callback-free absorptions gained +5–8.5% each, callback-dense ones ~1% or nothing): + +1. **Cross the PHP/C++ boundary per operation, never per element.** Absorb a + whole loop into one native call; a native loop invoking a PHP callback per + element performs like the PHP loop it replaced. +2. **Fast paths natively, callbacks only on slow paths** (pointer-compare + before `Type::equals()`, etc.). +3. **Resolve callables once per site** (`zend_function` pointers cached in + plans/caches). +4. **Third-party userland objects degrade per-operation, never per-element.** +5. **No materialization at the boundary** — operate on the engine's own + zvals/hashtables in place. This is also why the hot classes are registered + with the raw Zend API inside PHP-CPP's `onStartup` rather than through + PHP-CPP's call trampolines: `Php::Parameters` allocates a vector of + `Php::Value` per call, which is exactly the per-element boundary cost these + rules forbid. PHP-CPP hosts the extension lifecycle and the cold-path + `Runtime` class. +6. **Never shadow a DI-service class.** Nette's `getByType()` normalizes + requested types through reflection to the real class name and breaks + containers cached in the other mode. +7. **Every port must prove itself**: interleaved A/B benchmark on a long run + (user CPU, result cache cleared) plus a byte-identical output diff. Ports + measuring ≤0.5% get reverted — the failure mode is silent no-gain, and + unproven native code is pure maintenance debt. + +## Testing + +```bash +# differential test of the native classes vs the PHP implementations +php -d extension=$(pwd)/phpstan_turbo.so tests/smoke.php + +# PHPStan's own test suite with the extension loaded +php vendor/bin/phpunit ... + +# output identity (clear the result cache between runs!) +bin/phpstan analyse ... --error-format=raw # with extension +PHPSTAN_TURBO=0 bin/phpstan analyse ... # without ``` -Once the extension is loaded, PHPStan will use the extension automatically thanks to the `PHPStan\Turbo\TurboExtensionEnabler` class. +## History + +- The original proof of concept used Zephir (removed). +- The first full implementation was hand-written C (`phpize`); it is preserved + on the `turbo-c-extension` branch together with the matching PHPStan + sources, and this C++ version is its port. diff --git a/turbo-ext/analysis-stub.php b/turbo-ext/analysis-stub.php new file mode 100644 index 00000000000..c3d57119b0f --- /dev/null +++ b/turbo-ext/analysis-stub.php @@ -0,0 +1,22 @@ + $classMap + */ + public static function configure(array $classMap): void + { + } + +} diff --git a/turbo-ext/bin/generate-parser-actions.php b/turbo-ext/bin/generate-parser-actions.php new file mode 100644 index 00000000000..a8e4435cfb2 --- /dev/null +++ b/turbo-ext/bin/generate-parser-actions.php @@ -0,0 +1,1991 @@ +#!/usr/bin/env php + static function ($self, $stackPos) { BODY }` entry is + * extracted token-wise; `N => null` entries get no case (the engine + * applies the default action); + * - each BODY is whitespace-normalized and sha1-hashed; if + * src/parser/action-overrides/.inc exists its contents are emitted + * verbatim as the case body (hand-ported special cases; renumbering-safe + * because the key is the body content, not the rule number); + * - otherwise the BODY is transpiled statement-by-statement to C++ against + * the phpstanturbo::ParserEngine API in src/parser/ParserEngine.h (the + * actions are emitted as ParserEngine::reduceRange* member functions). + * Unsupported constructs make the whole run fail loudly, listing rule + * number, sha1 and PHP body — port those by adding an override file + * (never guess); + * - php-parser constants (Modifiers::*, Stmt\Use_::TYPE_*, ...) are + * resolved at generation time under the Composer autoloader and emitted + * as numeric values with a `/* Class::CONST *` `/` comment; + * - node construction policy: classes with subNodes-style or normalizing + * constructors call the real PHP constructor (newNodeCtor); everything + * else uses property-slot writes (newNode), verified at generation time + * against the reflected constructor body (trivial assignments only). + * + * The output is deterministic: the same vendored Php8.php and overrides + * produce byte-identical files (no timestamps). CI regenerates and diffs. + */ + +error_reporting(E_ALL); +ini_set('display_errors', 'stderr'); + +$root = dirname(__DIR__, 2); +require $root . '/vendor/autoload.php'; + +const PHP8_RELATIVE = 'vendor/nikic/php-parser/lib/PhpParser/Parser/Php8.php'; + +final class GenerateFailure extends Exception +{ + + /** @var list */ + public array $failures = []; + +} + +final class TranspileFailure extends Exception +{ + +} + +/** + * Token stream over one closure body (whitespace dropped, comments kept). + */ +final class Toks +{ + + /** @var list */ + private array $toks; + + private int $i = 0; + + public function __construct(string $phpBody) + { + $all = token_get_all('toks = array_values(array_filter($all, static function ($t): bool { + return !(is_array($t) && $t[0] === T_WHITESPACE); + })); + } + + /** @return array{int, string}|string|null */ + public function peek(int $ahead = 0) + { + return $this->toks[$this->i + $ahead] ?? null; + } + + /** @return array{int, string}|string */ + public function next() + { + if (!isset($this->toks[$this->i])) { + throw new TranspileFailure('unexpected end of body'); + } + return $this->toks[$this->i++]; + } + + public function atEnd(): bool + { + return $this->i >= count($this->toks); + } + + /** @param int|string $what token id or literal char */ + public function is($what, int $ahead = 0): bool + { + $t = $this->peek($ahead); + if ($t === null) { + return false; + } + if (is_string($what)) { + return $t === $what; + } + return is_array($t) && $t[0] === $what; + } + + public function isIdent(string $name, int $ahead = 0): bool + { + $t = $this->peek($ahead); + return is_array($t) && $t[0] === T_STRING && $t[1] === $name; + } + + public function isVar(string $name, int $ahead = 0): bool + { + $t = $this->peek($ahead); + return is_array($t) && $t[0] === T_VARIABLE && $t[1] === $name; + } + + /** @param int|string $what */ + public function tryConsume($what): bool + { + if ($this->is($what)) { + $this->i++; + return true; + } + return false; + } + + /** + * @param int|string $what + * @return array{int, string}|string + */ + public function expect($what) + { + if (!$this->is($what)) { + throw new TranspileFailure(sprintf( + 'expected %s, got %s', + is_string($what) ? var_export($what, true) : token_name($what), + $this->describe($this->peek()), + )); + } + return $this->next(); + } + + public function expectIdent(): string + { + $t = $this->expect(T_STRING); + return $t[1]; + } + + /** Member names after :: may tokenize as reserved keywords (Modifiers::ABSTRACT, ::FINAL, ...). */ + public function expectMemberName(): string + { + $t = $this->peek(); + if (is_array($t) && preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $t[1]) === 1) { + $this->next(); + return $t[1]; + } + throw new TranspileFailure('expected a member name, got ' . $this->describe($t)); + } + + public function expectInt(): int + { + $t = $this->expect(T_LNUMBER); + return (int) $t[1]; + } + + /** @param array{int, string}|string|null $t */ + private function describe($t): string + { + if ($t === null) { + return 'end of body'; + } + if (is_string($t)) { + return var_export($t, true); + } + return token_name($t[0]) . '(' . $t[1] . ')'; + } + +} + +/** + * A compiled expression fragment. + * + * kind: + * - slot: borrowed zval* (semStack slot, array element, property read) + * - owned: owned zval rvalue (builders, PN_NEW, helper calls) + * - owned_undef: owned zval rvalue that may be IS_UNDEF meaning PHP null + * - attrs: owned attributes array rvalue (consumed by builders) + * - long: zend_long rvalue + * - bool: C bool rvalue + * - pos: C int rvalue (stack positions, token positions) + * - null: PHP null literal + * - cstr: C string literal (already escaped, without quotes) + * - tokpos: token start/end stack reference (structured, see $tok) + */ +final class CExpr +{ + + public string $kind; + + public string $code = ''; + + /** @var list setup lines emitted before the line using $code */ + public array $pre = []; + + /** @var list teardown lines emitted after the line using $code */ + public array $post = []; + + /** @var array{which: string, n: int|null, m: int|null}|null for kind=tokpos */ + public ?array $tok = null; + + /** byte length of the (unescaped) value for kind=cstr */ + public int $valueLen = 0; + + public function __construct(string $kind, string $code = '') + { + $this->kind = $kind; + $this->code = $code; + } + +} + +final class Transpiler +{ + + /** Classes whose PHP constructor must be called (PN_NEW_CTOR): subNodes-style or normalizing ctors. */ + private const CTOR_CLASSES = [ + 'PhpParser\Node\Stmt\If_' => true, + 'PhpParser\Node\Stmt\For_' => true, + 'PhpParser\Node\Stmt\Foreach_' => true, + 'PhpParser\Node\Stmt\Function_' => true, + 'PhpParser\Node\Stmt\Class_' => true, + 'PhpParser\Node\Stmt\Interface_' => true, + 'PhpParser\Node\Stmt\Trait_' => true, + 'PhpParser\Node\Stmt\Enum_' => true, + 'PhpParser\Node\Stmt\ClassMethod' => true, + 'PhpParser\Node\Stmt\EnumCase' => true, + 'PhpParser\Node\Stmt\TraitUseAdaptation\Precedence' => true, + 'PhpParser\Node\Stmt\TraitUseAdaptation\Alias' => true, + 'PhpParser\Node\PropertyHook' => true, + 'PhpParser\Node\DeclareItem' => true, + 'PhpParser\Node\Expr\Closure' => true, + 'PhpParser\Node\Expr\ArrowFunction' => true, + ]; + + /** + * Classes with a non-trivial constructor where property-slot writes are + * nevertheless correct for every grammar call site (the normalization + * can never fire on parser-produced values). Mirrors the hand-written + * port's per-class analysis. + */ + private const FORCED_SLOT_CLASSES = [ + 'PhpParser\Node\Identifier' => 'ctor only throws on an empty name; the grammar never produces one', + 'PhpParser\Node\VarLikeIdentifier' => 'inherits Identifier\'s ctor; the grammar never produces an empty name', + 'PhpParser\Node\UseItem' => 'is_string($alias) normalization never fires: alias is null or an Identifier node', + 'PhpParser\Node\Const_' => 'is_string($name) normalization never fires: name is an Identifier node', + 'PhpParser\Node\PropertyItem' => 'is_string($name) normalization never fires: name is a VarLikeIdentifier node', + 'PhpParser\Node\Stmt\Goto_' => 'is_string($name) normalization never fires: name is an Identifier node', + 'PhpParser\Node\Stmt\Label' => 'is_string($name) normalization never fires: name is an Identifier node', + 'PhpParser\Node\Expr\PropertyFetch' => 'is_string($name) normalization never fires: name is an Identifier/Expr node', + 'PhpParser\Node\Expr\NullsafePropertyFetch' => 'is_string($name) normalization never fires: name is an Identifier/Expr node', + 'PhpParser\Node\Expr\MethodCall' => 'is_string($name) normalization never fires: name is an Identifier/Expr node', + 'PhpParser\Node\Expr\NullsafeMethodCall' => 'is_string($name) normalization never fires: name is an Identifier/Expr node', + 'PhpParser\Node\Expr\StaticCall' => 'is_string($name) normalization never fires: name is an Identifier/Expr/Variable node', + 'PhpParser\Node\Expr\StaticPropertyFetch' => 'is_string($name) normalization never fires: name is a VarLikeIdentifier/Expr node', + 'PhpParser\Node\Expr\ClassConstFetch' => 'is_string($name) normalization never fires: name is an Identifier/Expr/Error node', + ]; + + /** PhpVersion query methods are fixed thresholds on PhpVersion->id (see vendor PhpVersion.php). */ + private const PHP_VERSION_METHODS = [ + 'supportsUnicodeEscapes' => 'phpVersionId >= 70000', + 'allowsInvalidOctals' => 'phpVersionId < 70000', + 'allowsAssignNewByReference' => 'phpVersionId < 70000', + ]; + + /** + * $self helper methods returning a value; the ParserEngine methods carry + * the same names, so the map only holds arg specs and the return kind. + * arg specs: zvp (borrowed zv::Ref), attrs (owned zv::Arr), pos (C int), bool + */ + private const VALUE_METHODS = [ + 'handleNamespaces' => [['zvp'], 'owned'], + 'handleBuiltinTypes' => [['zvp'], 'owned'], + 'handleHaltCompiler' => [[], 'owned'], + 'maybeCreateNop' => [['pos', 'pos'], 'owned_undef'], + 'maybeCreateZeroLengthNop' => [['pos'], 'owned_undef'], + 'inlineHtmlHasLeadingNewline' => [['pos'], 'bool'], + 'fixupArrayDestructuring' => [['zvp'], 'owned'], + 'parseLNumber' => [['zvp', 'attrs', 'bool'], 'owned'], + 'parseNumString' => [['zvp', 'attrs'], 'owned'], + 'parseDocString' => [['zvp', 'zvp', 'zvp', 'attrs', 'attrs', 'bool'], 'owned'], + 'createExitExpr' => [['zvp', 'pos', 'zvp', 'attrs'], 'owned'], + 'getIntCastKind' => [['zvp'], 'long'], + 'getFloatCastKind' => [['zvp'], 'long'], + 'getBoolCastKind' => [['zvp'], 'long'], + 'getStringCastKind' => [['zvp'], 'long'], + ]; + + /** + * $self helper methods called as statements (void); the ParserEngine + * method names match the PHP names. + * arg specs: zvp, pos, long (slot coerced via zv::Ref::toLong()) + */ + private const VOID_METHODS = [ + 'checkClassModifier' => [['long', 'long', 'pos']], + 'checkModifier' => [['long', 'long', 'pos']], + 'checkPropertyHookModifiers' => [['long', 'long', 'pos']], + 'checkParam' => [['zvp']], + 'checkTryCatch' => [['zvp']], + 'checkNamespace' => [['zvp']], + 'checkClass' => [['zvp', 'pos']], + 'checkInterface' => [['zvp', 'pos']], + 'checkEnum' => [['zvp', 'pos']], + 'checkClassMethod' => [['zvp', 'pos']], + 'checkClassConst' => [['zvp', 'pos']], + 'checkUseUse' => [['zvp', 'pos']], + 'checkPropertyHooksForMultiProperty' => [['zvp', 'pos']], + 'checkEmptyPropertyHookList' => [['zvp', 'pos']], + 'checkConstantAttributes' => [['zvp']], + 'checkPipeOperatorParentheses' => [['zvp']], + 'addPropertyNameToHooks' => [['zvp']], + 'fixupAlternativeElse' => [['zvp']], + 'postprocessList' => [['zvp']], + // checkPropertyHook is special-cased: (?int $paramListPos) maps to (int, bool hasParamList) + ]; + + /** @var array alias => FQCN prefix, parsed from Php8.php's use statements */ + private array $imports; + + private Toks $toks; + + private int $tmpCounter = 0; + + private int $subCounter = 0; + + /** @var array declared local attrs variables => consumed yet */ + private array $attrsLocals = []; + + /** @var array memoized ctor triviality per FQCN */ + private static array $ctorTriviality = []; + + /** @param array $imports */ + public function __construct(array $imports) + { + $this->imports = $imports; + } + + /** + * @return list fully indented case-body lines (without `return true;`) + * @throws TranspileFailure + */ + public function transpile(string $body): array + { + $this->toks = new Toks($body); + $this->tmpCounter = 0; + $this->subCounter = 0; + $this->attrsLocals = []; + $lines = []; + while (!$this->toks->atEnd()) { + foreach ($this->parseStatement(2) as $line) { + $lines[] = $line; + } + } + foreach ($this->attrsLocals as $name => $consumed) { + if (!$consumed) { + throw new TranspileFailure(sprintf('local $%s (attrs) is never consumed', $name)); + } + } + return $lines; + } + + private function tmp(): string + { + $this->tmpCounter++; + return 't' . $this->tmpCounter; + } + + private function subName(): string + { + $this->subCounter++; + return 'sub' . $this->subCounter; + } + + private function fail(string $message): void + { + throw new TranspileFailure($message); + } + + // ===== statements ===== + + /** @return list */ + private function parseStatement(int $indent): array + { + $t = $this->toks->peek(); + if ($t === ';') { + $this->toks->next(); + return []; + } + if (is_array($t) && ($t[0] === T_COMMENT || $t[0] === T_DOC_COMMENT)) { + $this->toks->next(); + if (strpos($t[1], '/*') !== 0) { + $this->fail('unsupported comment style: ' . $t[1]); + } + return [$this->ind($indent) . preg_replace('/\s+/', ' ', $t[1])]; + } + if (is_array($t) && $t[0] === T_IF) { + return $this->parseIf($indent); + } + if (is_array($t) && $t[0] === T_THROW) { + return $this->parseThrow($indent); + } + if (is_array($t) && $t[0] === T_VARIABLE) { + if ($t[1] === '$self') { + return $this->parseSelfStatement($indent); + } + return $this->parseLocalStatement($indent); + } + $this->fail('unsupported statement start: ' . (is_array($t) ? token_name($t[0]) . '(' . $t[1] . ')' : var_export($t, true))); + return []; + } + + /** @return list */ + private function parseThrow(int $indent): array + { + $this->toks->expect(T_THROW); + $this->toks->expect(T_NEW); + $name = $this->parseClassName(); + if ($this->resolveFqcn($name) !== 'PhpParser\Error') { + $this->fail('throw of unsupported class ' . $name); + } + $this->toks->expect('('); + $msg = $this->parseExpr(); + if ($msg->kind !== 'cstr') { + $this->fail('throw new Error() message must be a string literal'); + } + $this->toks->expect(','); + $attrs = $this->parseExpr(); + if ($attrs->kind !== 'attrs') { + $this->fail('throw new Error() attributes must be an attributes expression'); + } + $this->toks->expect(')'); + $this->toks->expect(';'); + return [$this->ind($indent) . sprintf('fatalError("%s", %s);', $msg->code, $attrs->code)]; + } + + /** @return list */ + private function parseSelfStatement(int $indent): array + { + $this->toks->expect(T_VARIABLE); // $self + $this->toks->expect(T_OBJECT_OPERATOR); + $name = $this->toks->expectIdent(); + + if ($name === 'semValue') { + return $this->parseSemValueStatement($indent); + } + if ($name === 'semStack') { + // $self->semStack[POS][] = EXPR; + [$n, $m] = $this->parseSemStackIndex(); + $slot = sprintf('PN_SEM(%d, %d)', $n, $m); + $this->toks->expect('['); + $this->toks->expect(']'); + $this->toks->expect('='); + $rhs = $this->parseExpr(); + $this->toks->expect(';'); + if ($rhs->kind !== 'slot') { + $this->fail('semStack push of non-slot value (kind ' . $rhs->kind . ')'); + } + return [$this->ind($indent) . sprintf('pushOnto(%s, %s);', $slot, $rhs->code)]; + } + if ($name === 'errorState') { + $this->toks->expect('='); + $value = $this->toks->expectInt(); + $this->toks->expect(';'); + return [$this->ind($indent) . sprintf('errorState = %d;', $value)]; + } + if ($name === 'createdArrays' || $name === 'parenthesizedArrowFunctions') { + $this->toks->expect(T_OBJECT_OPERATOR); + $method = $this->toks->expectIdent(); + if ($method !== 'offsetSet') { + $this->fail(sprintf('unsupported %s->%s()', $name, $method)); + } + $this->toks->expect('('); + $arg = $this->parseExpr(); + $this->toks->expect(')'); + $this->toks->expect(';'); + if ($arg->code !== 'semValue.ref()') { + $this->fail($name . '->offsetSet() on something else than $self->semValue'); + } + $fn = $name === 'createdArrays' ? 'createdArraysAdd' : 'parenthesizedArrowFunctionsAdd'; + return [$this->ind($indent) . sprintf('%s(semValue.ref());', $fn)]; + } + if ($name === 'emitError') { + return $this->parseEmitError($indent); + } + if ($name === 'checkPropertyHook') { + $this->toks->expect('('); + $node = $this->parseExpr(); + $this->toks->expect(','); + if ($this->toks->isIdent('null')) { + $this->toks->next(); + $posCode = '0, false'; + } else { + $pos = $this->parseExpr(); + if ($pos->kind !== 'pos') { + $this->fail('checkPropertyHook() second argument must be a position'); + } + $posCode = $pos->code . ', true'; + } + $this->toks->expect(')'); + $this->toks->expect(';'); + return [$this->ind($indent) . sprintf('checkPropertyHook(%s, %s);', $this->toZvpSimple($node, 'checkPropertyHook'), $posCode)]; + } + if (isset(self::VOID_METHODS[$name])) { + [$specs] = self::VOID_METHODS[$name]; + $this->toks->expect('('); + $args = []; + foreach ($specs as $i => $spec) { + if ($i > 0) { + $this->toks->expect(','); + } + $args[] = $this->convertArg($this->parseExpr(), $spec, $name); + } + $this->toks->expect(')'); + $this->toks->expect(';'); + return [$this->ind($indent) . sprintf('%s(%s);', $name, implode(', ', $args))]; + } + $this->fail('unsupported $self member in statement position: ' . $name); + return []; + } + + /** @return list */ + private function parseSemValueStatement(int $indent): array + { + if ($this->toks->is(T_OBJECT_OPERATOR)) { + $this->toks->next(); + $member = $this->toks->expectIdent(); + if ($member === 'setAttribute' && $this->toks->is('(')) { + $this->toks->next(); + $key = $this->parseExpr(); + if ($key->kind !== 'cstr') { + $this->fail('setAttribute() key must be a string literal'); + } + $this->toks->expect(','); + $value = $this->parseExpr(); + $this->toks->expect(')'); + $this->toks->expect(';'); + $owned = $this->toOwned($value, 'setAttribute value'); + $lines = $this->indentAll($value->pre, $indent); + $lines[] = $this->ind($indent) . sprintf('setNodeAttribute(semValue.ref(), "%s", %s);', $key->code, $owned); + return array_merge($lines, $this->indentAll($value->post, $indent)); + } + // $self->semValue->PROP = EXPR; + $this->toks->expect('='); + $rhs = $this->parseExpr(); + $this->toks->expect(';'); + $owned = $this->toOwned($rhs, 'property write value'); + $lines = $this->indentAll($rhs->pre, $indent); + $lines[] = $this->ind($indent) . sprintf('propWrite(semValue.ref(), "%s", %s);', $member, $owned); + return array_merge($lines, $this->indentAll($rhs->post, $indent)); + } + + $this->toks->expect('='); + $rhs = $this->parseExpr(); + $this->toks->expect(';'); + $lines = $this->indentAll($rhs->pre, $indent); + switch ($rhs->kind) { + case 'slot': + case 'owned': + case 'attrs': + $lines[] = $this->ind($indent) . sprintf('semValue = %s;', $rhs->code); + break; + case 'owned_undef': + $tmp = $this->tmp(); + $lines[] = $this->ind($indent) . sprintf('zv::Val %s = %s;', $tmp, $rhs->code); + $lines[] = $this->ind($indent) . sprintf('if (%s.isUndef()) {', $tmp); + $lines[] = $this->ind($indent + 1) . sprintf('%s = zv::Val::null(); /* PHP null return */', $tmp); + $lines[] = $this->ind($indent) . '}'; + $lines[] = $this->ind($indent) . sprintf('semValue = std::move(%s);', $tmp); + break; + case 'long': + $lines[] = $this->ind($indent) . sprintf('semValue = zv::Val::integer(%s);', $rhs->code); + break; + case 'bool': + $lines[] = $this->ind($indent) . sprintf('semValue = zv::Val::boolean(%s);', $rhs->code); + break; + case 'null': + $lines[] = $this->ind($indent) . 'semValue = zv::Val::null();'; + break; + default: + $this->fail('unsupported semValue assignment of kind ' . $rhs->kind); + } + return array_merge($lines, $this->indentAll($rhs->post, $indent)); + } + + /** @return list */ + private function parseEmitError(int $indent): array + { + // already consumed: $self->emitError + $this->toks->expect('('); + $this->toks->expect(T_NEW); + $name = $this->parseClassName(); + if ($this->resolveFqcn($name) !== 'PhpParser\Error') { + $this->fail('emitError() of unsupported class ' . $name); + } + $this->toks->expect('('); + $msg = $this->parseExpr(); + if ($msg->kind !== 'cstr') { + $this->fail('emitError() message must be a string literal'); + } + $this->toks->expect(','); + $attrs = $this->parseExpr(); + if ($attrs->kind !== 'attrs') { + $this->fail('emitError() attributes must be an attributes expression'); + } + $this->toks->expect(')'); + $this->toks->expect(')'); + $this->toks->expect(';'); + return [$this->ind($indent) . sprintf('emitError("%s", %s);', $msg->code, $attrs->code)]; + } + + /** @return list */ + private function parseLocalStatement(int $indent): array + { + $t = $this->toks->expect(T_VARIABLE); + $name = substr($t[1], 1); + if ($this->toks->is('[')) { + // $attrs['key'] = EXPR; + if (!isset($this->attrsLocals[$name])) { + $this->fail('index write to undeclared local $' . $name); + } + $this->toks->next(); + $key = $this->parseExpr(); + if ($key->kind !== 'cstr') { + $this->fail('local array write key must be a string literal'); + } + $this->toks->expect(']'); + $this->toks->expect('='); + $rhs = $this->parseExpr(); + $this->toks->expect(';'); + $owned = $this->toOwned($rhs, 'attrs value'); + $lines = $this->indentAll($rhs->pre, $indent); + $lines[] = $this->ind($indent) . sprintf('%s.set("%s", %s);', $name, $key->code, $owned); + return array_merge($lines, $this->indentAll($rhs->post, $indent)); + } + $this->toks->expect('='); + $rhs = $this->parseExpr(); + $this->toks->expect(';'); + if ($rhs->kind !== 'attrs') { + $this->fail(sprintf('local $%s holds unsupported kind %s (only attributes arrays are supported)', $name, $rhs->kind)); + } + if (isset($this->attrsLocals[$name])) { + $this->fail('local $' . $name . ' redeclared'); + } + $this->attrsLocals[$name] = false; + $lines = $this->indentAll($rhs->pre, $indent); + $lines[] = $this->ind($indent) . sprintf('zv::Arr %s = %s;', $name, $rhs->code); + return array_merge($lines, $this->indentAll($rhs->post, $indent)); + } + + /** @return list */ + private function parseIf(int $indent): array + { + $this->toks->expect(T_IF); + $this->toks->expect('('); + $cond = $this->parseCondition(); + $this->toks->expect(')'); + $lines = [$this->ind($indent) . sprintf('if (%s) {', $cond)]; + foreach ($this->parseBranch($indent + 1) as $line) { + $lines[] = $line; + } + while (true) { + if ($this->toks->is(T_ELSEIF) || ($this->toks->is(T_ELSE) && $this->toks->is(T_IF, 1))) { + if ($this->toks->tryConsume(T_ELSEIF) === false) { + $this->toks->expect(T_ELSE); + $this->toks->expect(T_IF); + } + $this->toks->expect('('); + $cond = $this->parseCondition(); + $this->toks->expect(')'); + $lines[] = $this->ind($indent) . sprintf('} else if (%s) {', $cond); + foreach ($this->parseBranch($indent + 1) as $line) { + $lines[] = $line; + } + continue; + } + if ($this->toks->is(T_ELSE)) { + $this->toks->next(); + $lines[] = $this->ind($indent) . '} else {'; + foreach ($this->parseBranch($indent + 1) as $line) { + $lines[] = $line; + } + continue; + } + break; + } + $lines[] = $this->ind($indent) . '}'; + return $lines; + } + + /** @return list */ + private function parseBranch(int $indent): array + { + if ($this->toks->tryConsume('{')) { + $lines = []; + while (!$this->toks->is('}')) { + foreach ($this->parseStatement($indent) as $line) { + $lines[] = $line; + } + } + $this->toks->expect('}'); + return $lines; + } + return $this->parseStatement($indent); + } + + private function parseCondition(): string + { + $negated = false; + if ($this->toks->tryConsume('!')) { + $negated = true; + } + $lhs = $this->parseExpr(); + $code = null; + if ($this->toks->is(T_IS_IDENTICAL) || $this->toks->is(T_IS_NOT_IDENTICAL)) { + $isNot = $this->toks->is(T_IS_NOT_IDENTICAL); + $this->toks->next(); + $zvp = $this->toZvpSimple($lhs, 'comparison'); + if ($this->toks->isIdent('null')) { + $this->toks->next(); + $code = sprintf('%s.isNull()', $zvp); + } elseif ($this->toks->is(T_CONSTANT_ENCAPSED_STRING)) { + $str = $this->parseExpr(); + $code = sprintf('%s.stringEquals("%s")', $zvp, $str->code); + if ($isNot) { + $code = sprintf('!(%s)', $code); + $isNot = false; + } + } else { + $this->fail('unsupported comparison operand in condition'); + } + if ($isNot) { + $code = '!' . $code; + } + } elseif ($this->toks->is(T_INSTANCEOF)) { + $this->toks->next(); + $name = $this->parseClassName(); + $this->resolveFqcn($name); // validate it resolves + $code = sprintf('isInstanceOf(%s, "%s")', $this->toZvpSimple($lhs, 'instanceof'), $this->cAlias($name)); + } else { + if ($lhs->kind !== 'bool') { + $this->fail('unsupported bare condition of kind ' . $lhs->kind); + } + $code = $lhs->code; + } + if ($lhs->pre !== [] || $lhs->post !== []) { + $this->fail('condition operand requires temporaries'); + } + if ($negated) { + $code = sprintf('!(%s)', $code); + } + return $code; + } + + // ===== expressions ===== + + private function parseExpr(): CExpr + { + $expr = $this->parsePrimary(); + // only the modifier-mask `A | B` binary occurs in the closures + while ($this->toks->is('|')) { + $this->toks->next(); + $rhs = $this->parsePrimary(); + $expr = new CExpr('long', sprintf('%s | %s', $this->toLong($expr), $this->toLong($rhs))); + } + return $expr; + } + + private function parsePrimary(): CExpr + { + $t = $this->toks->peek(); + + if ($t === '-') { + $this->toks->next(); + $value = $this->toks->expectInt(); + return new CExpr('long', (string) -$value); + } + if (is_array($t) && $t[0] === T_LNUMBER) { + $this->toks->next(); + return new CExpr('long', (string) (int) $t[1]); + } + if (is_array($t) && $t[0] === T_CONSTANT_ENCAPSED_STRING) { + $this->toks->next(); + $value = $this->phpStringValue($t[1]); + $expr = new CExpr('cstr', $this->cEscape($value)); + $expr->valueLen = strlen($value); + return $expr; + } + if (is_array($t) && $t[0] === T_VARIABLE) { + return $this->parseVariableExpr(); + } + if (is_array($t) && $t[0] === T_NEW) { + return $this->parseNew(); + } + if (is_array($t) && $t[0] === T_ARRAY) { + $this->toks->next(); + $this->toks->expect('('); + return $this->parseArrayLiteral(')'); + } + if ($t === '[') { + $this->toks->next(); + return $this->parseArrayLiteral(']'); + } + if (is_array($t) && $t[0] === T_STRING) { + if ($t[1] === 'null') { + $this->toks->next(); + return new CExpr('null'); + } + if ($t[1] === 'true') { + $this->toks->next(); + return new CExpr('bool', 'true'); + } + if ($t[1] === 'false') { + $this->toks->next(); + return new CExpr('bool', 'false'); + } + if ($t[1] === 'substr') { + return $this->parseSubstr(); + } + } + if (is_array($t) && in_array($t[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) { + // Class::CONST or Class::staticMethod(...) + return $this->parseStaticAccess(); + } + $this->fail('unsupported expression start: ' . (is_array($t) ? token_name($t[0]) . '(' . $t[1] . ')' : var_export($t, true))); + return new CExpr('null'); + } + + private function parseSubstr(): CExpr + { + $this->toks->expect(T_STRING); // substr + $this->toks->expect('('); + $subject = $this->parseExpr(); + if ($subject->kind !== 'slot') { + $this->fail('substr() subject must be a semStack slot'); + } + $this->toks->expect(','); + $offset = $this->parseExpr(); + if ($offset->kind !== 'long') { + $this->fail('substr() offset must be an integer literal'); + } + $this->toks->expect(')'); + return new CExpr('owned', sprintf('substr(%s, %s)', $subject->code, $offset->code)); + } + + private function parseStaticAccess(): CExpr + { + $name = $this->parseClassName(); + $this->toks->expect(T_DOUBLE_COLON); + $member = $this->toks->expectMemberName(); + $fqcn = $this->resolveFqcn($name); + if ($this->toks->is('(')) { + if ($fqcn === 'PhpParser\Node\Scalar\String_' && $member === 'fromString') { + $this->toks->expect('('); + $raw = $this->convertArg($this->parseExpr(), 'zvp', 'String_::fromString'); + $this->toks->expect(','); + $attrs = $this->convertArg($this->parseExpr(), 'attrs', 'String_::fromString'); + $this->toks->expect(','); + $unicode = $this->convertArg($this->parseExpr(), 'bool', 'String_::fromString'); + $this->toks->expect(')'); + return new CExpr('owned', sprintf('stringFromString(%s, %s, %s)', $raw, $attrs, $unicode)); + } + if ($fqcn === 'PhpParser\Node\Scalar\Float_' && $member === 'fromString') { + $this->toks->expect('('); + $raw = $this->convertArg($this->parseExpr(), 'zvp', 'Float_::fromString'); + $this->toks->expect(','); + $attrs = $this->convertArg($this->parseExpr(), 'attrs', 'Float_::fromString'); + $this->toks->expect(')'); + return new CExpr('owned', sprintf('floatFromString(%s, %s)', $raw, $attrs)); + } + $this->fail(sprintf('unsupported static method call %s::%s()', $name, $member)); + } + if (!defined($fqcn . '::' . $member)) { + $this->fail(sprintf('cannot resolve constant %s::%s (FQCN %s)', $name, $member, $fqcn)); + } + $value = constant($fqcn . '::' . $member); + if (!is_int($value)) { + $this->fail(sprintf('constant %s::%s is not an integer', $name, $member)); + } + return new CExpr('long', sprintf('%d /* %s::%s */', $value, $name, $member)); + } + + private function parseVariableExpr(): CExpr + { + $t = $this->toks->expect(T_VARIABLE); + $var = $t[1]; + + if ($var === '$stackPos') { + if ($this->toks->is('-') && $this->toks->is('(', 1)) { + $this->toks->expect('-'); + $this->toks->expect('('); + $n = $this->toks->expectInt(); + $this->toks->expect('-'); + $m = $this->toks->expectInt(); + $this->toks->expect(')'); + return new CExpr('pos', sprintf('stackPos - (%d - %d)', $n, $m)); + } + return new CExpr('pos', 'stackPos'); + } + + if ($var === '$self') { + $this->toks->expect(T_OBJECT_OPERATOR); + $member = $this->toks->expectIdent(); + return $this->parseSelfMemberExpr($member); + } + + // locals: only declared attrs arrays are supported in expressions + $name = substr($var, 1); + if (isset($this->attrsLocals[$name])) { + if ($this->attrsLocals[$name]) { + $this->fail(sprintf('local $%s (attrs) consumed twice', $name)); + } + $this->attrsLocals[$name] = true; + return new CExpr('attrs', sprintf('std::move(%s)', $name)); + } + $this->fail('unsupported local variable in expression: ' . $var); + return new CExpr('null'); + } + + private function parseSelfMemberExpr(string $member): CExpr + { + if ($member === 'semStack') { + [$n, $m] = $this->parseSemStackIndex(); + $expr = new CExpr('slot', sprintf('PN_SEM(%d, %d)', $n, $m)); + return $this->parseSlotSuffixes($expr); + } + if ($member === 'semValue') { + $expr = new CExpr('slot', 'semValue.ref()'); + return $this->parseSlotSuffixes($expr); + } + if ($member === 'tokenPos') { + return new CExpr('pos', 'tokenPos'); + } + if ($member === 'tokenStartStack' || $member === 'tokenEndStack') { + $which = $member === 'tokenStartStack' ? 'start' : 'end'; + $this->toks->expect('['); + $pos = $this->parseExpr(); + $this->toks->expect(']'); + $expr = new CExpr('tokpos'); + if ($pos->code === 'stackPos') { + $expr->tok = ['which' => $which, 'n' => null, 'm' => null]; + $expr->code = sprintf('%s[stackPos]', $member); + } elseif (preg_match('/^stackPos - \((\d+) - (\d+)\)$/', $pos->code, $mm) === 1) { + $expr->tok = ['which' => $which, 'n' => (int) $mm[1], 'm' => (int) $mm[2]]; + $expr->code = sprintf('%s(%d, %d)', $which === 'start' ? 'PN_TOKSTART' : 'PN_TOKEND', $mm[1], $mm[2]); + } else { + $this->fail('unsupported token stack index: ' . $pos->code); + } + return $expr; + } + if ($member === 'getAttributes') { + $this->toks->expect('('); + $a = $this->parseExpr(); + $this->toks->expect(','); + $b = $this->parseExpr(); + $this->toks->expect(')'); + if ($a->kind !== 'tokpos' || $b->kind !== 'tokpos' || $a->tok['which'] !== 'start' || $b->tok['which'] !== 'end') { + $this->fail('getAttributes() arguments must be tokenStartStack/tokenEndStack expressions'); + } + if ($a->tok['n'] === null) { + $this->fail('getAttributes() with a bare $stackPos start position'); + } + $n = $a->tok['n']; + $m1 = $a->tok['m']; + if ($b->tok['n'] === null) { + $m2 = $n; // tokenEndStack[$stackPos] == PN_TOKEND(n, n) + } else { + // PN_ATTRS only uses the difference n-m; renormalize onto $n + $m2 = $n - ($b->tok['n'] - $b->tok['m']); + } + return new CExpr('attrs', sprintf('PN_ATTRS(%d, %d, %d)', $n, $m1, $m2)); + } + if ($member === 'phpVersion') { + $this->toks->expect(T_OBJECT_OPERATOR); + $method = $this->toks->expectIdent(); + $this->toks->expect('('); + $this->toks->expect(')'); + if (!isset(self::PHP_VERSION_METHODS[$method])) { + $this->fail('unsupported PhpVersion method: ' . $method); + } + return new CExpr('bool', sprintf('%s /* PhpVersion::%s() */', self::PHP_VERSION_METHODS[$method], $method)); + } + if (isset(self::VALUE_METHODS[$member])) { + [$specs, $retKind] = self::VALUE_METHODS[$member]; + $this->toks->expect('('); + $args = []; + $pre = []; + $post = []; + foreach ($specs as $i => $spec) { + if ($i > 0) { + $this->toks->expect(','); + } + $arg = $this->parseExpr(); + $args[] = $this->convertArgCollect($arg, $spec, $member, $pre, $post); + } + $this->toks->expect(')'); + $expr = new CExpr($retKind, sprintf('%s(%s)', $member, implode(', ', $args))); + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + $this->fail('unsupported $self member in expression: ' . $member); + return new CExpr('null'); + } + + private function parseSlotSuffixes(CExpr $expr): CExpr + { + while (true) { + if ($this->toks->is('[')) { + $this->toks->next(); + $idx = $this->toks->expectInt(); + $this->toks->expect(']'); + $expr = $this->withCarried($expr, new CExpr('slot', sprintf('itemAt(%s, %d)', $expr->code, $idx))); + continue; + } + if ($this->toks->is(T_OBJECT_OPERATOR) && !$this->toks->is('(', 2)) { + $this->toks->next(); + $prop = $this->toks->expectIdent(); + $expr = $this->withCarried($expr, new CExpr('slot', sprintf('prop(%s, "%s")', $expr->code, $prop))); + continue; + } + break; + } + return $expr; + } + + private function withCarried(CExpr $old, CExpr $new): CExpr + { + $new->pre = $old->pre; + $new->post = $old->post; + return $new; + } + + /** @return array{int, int} */ + private function parseSemStackIndex(): array + { + $this->toks->expect('['); + $this->toks->expect(T_VARIABLE); // $stackPos + $this->toks->expect('-'); + $this->toks->expect('('); + $n = $this->toks->expectInt(); + $this->toks->expect('-'); + $m = $this->toks->expectInt(); + $this->toks->expect(')'); + $this->toks->expect(']'); + return [$n, $m]; + } + + private function parseArrayLiteral(string $closer): CExpr + { + if ($this->toks->tryConsume($closer)) { + return new CExpr('owned', 'zv::Arr::empty()'); + } + // assoc (subNodes-style) if the first element is 'key' => + if ($this->toks->is(T_CONSTANT_ENCAPSED_STRING) && $this->toks->is(T_DOUBLE_ARROW, 1)) { + return $this->parseSubNodesLiteral($closer); + } + $elements = []; + while (true) { + $elements[] = $this->parseExpr(); + if ($this->toks->tryConsume(',')) { + if ($this->toks->tryConsume($closer)) { + break; + } + continue; + } + $this->toks->expect($closer); + break; + } + if (count($elements) > 2) { + $this->fail('positional array literal with more than 2 elements'); + } + $pre = []; + $post = []; + $args = []; + foreach ($elements as $element) { + // arrayOf() borrows every element, so PHP null becomes an owned + // zv::Val::null() temporary (nullptr is only valid for node props) + $args[] = $this->toBorrowedCollect($element, 'array element', $pre, $post, false); + } + $expr = new CExpr('owned', count($args) === 1 + ? sprintf('arrayOf(%s)', $args[0]) + : sprintf('arrayOf(%s, %s)', $args[0], $args[1])); + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + + private function parseSubNodesLiteral(string $closer): CExpr + { + $sub = $this->subName(); + $pre = [sprintf('zv::Arr %s = zv::Arr::empty();', $sub)]; + $post = []; + while (true) { + $keyTok = $this->toks->expect(T_CONSTANT_ENCAPSED_STRING); + $key = $this->phpStringValue($keyTok[1]); + $this->toks->expect(T_DOUBLE_ARROW); + $value = $this->parseExpr(); + foreach ($value->pre as $line) { + $pre[] = $line; + } + $pre[] = sprintf('%s.set("%s", %s);', $sub, $this->cEscape($key), $this->toOwned($value, 'subNodes value')); + foreach ($value->post as $line) { + $pre[] = $line; + } + if ($this->toks->tryConsume(',')) { + if ($this->toks->tryConsume($closer)) { + break; + } + continue; + } + $this->toks->expect($closer); + break; + } + $expr = new CExpr('subarr', $sub); + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + + private function parseNew(): CExpr + { + $this->toks->expect(T_NEW); + $srcName = $this->parseClassName(); + $fqcn = $this->resolveFqcn($srcName); + $this->toks->expect('('); + /** @var list $args */ + $args = []; + if (!$this->toks->tryConsume(')')) { + while (true) { + $args[] = $this->parseExpr(); + if ($this->toks->tryConsume(',')) { + if ($this->toks->tryConsume(')')) { + break; + } + continue; + } + $this->toks->expect(')'); + break; + } + } + + if ($fqcn === 'PhpParser\Node\Name') { + return $this->buildNameCall('newName(%s, %s)', $srcName, null, $args); + } + if ($fqcn === 'PhpParser\Node\Name\FullyQualified' || $fqcn === 'PhpParser\Node\Name\Relative') { + $alias = $fqcn === 'PhpParser\Node\Name\FullyQualified' ? 'Name\\\\FullyQualified' : 'Name\\\\Relative'; + return $this->buildNameCall('newNameVariant("' . $alias . '", %s, %s)', $srcName, $alias, $args); + } + + $ctorParams = $this->reflectCtorParams($fqcn, $srcName); + if (isset(self::CTOR_CLASSES[$fqcn])) { + return $this->buildCtorNew($srcName, $fqcn, $ctorParams, $args); + } + return $this->buildSlotsNew($srcName, $fqcn, $ctorParams, $args); + } + + /** @param list $args */ + private function buildNameCall(string $format, string $srcName, ?string $alias, array $args): CExpr + { + if (count($args) !== 2) { + $this->fail(sprintf('new %s with %d args (expected 2)', $srcName, count($args))); + } + $pre = []; + $post = []; + $strOrParts = $this->toRefCollect($args[0], 'Name argument', $pre, $post); + if ($args[1]->kind !== 'attrs') { + $this->fail(sprintf('new %s second argument must be an attributes expression', $srcName)); + } + foreach ($args[1]->pre as $line) { + $pre[] = $line; + } + foreach ($args[1]->post as $line) { + $post[] = $line; + } + $expr = new CExpr('owned', sprintf($format, $strOrParts, $args[1]->code)); + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + + /** + * @param list $ctorParams + * @param list $args + */ + private function buildCtorNew(string $srcName, string $fqcn, array $ctorParams, array $args): CExpr + { + // newNodeCtor passes props then attrs as the constructor arguments, so + // the call shape must be exact: every parameter supplied, $attributes last. + if (count($args) !== count($ctorParams)) { + $this->fail(sprintf( + 'new %s (ctor mode) with %d args but the constructor has %d parameters', + $srcName, + count($args), + count($ctorParams), + )); + } + $last = $ctorParams[count($ctorParams) - 1]; + if ($last->getName() !== 'attributes') { + $this->fail(sprintf('new %s (ctor mode): last constructor parameter is not $attributes', $srcName)); + } + $attrsExpr = $args[count($args) - 1]; + if ($attrsExpr->kind !== 'attrs') { + $this->fail(sprintf('new %s (ctor mode): last argument is not an attributes expression', $srcName)); + } + $pre = []; + $post = []; + $propArgs = []; + for ($i = 0; $i < count($args) - 1; $i++) { + $propArgs[] = $this->toBorrowedCollect($args[$i], 'constructor argument', $pre, $post); + } + foreach ($attrsExpr->pre as $line) { + $pre[] = $line; + } + foreach ($attrsExpr->post as $line) { + $post[] = $line; + } + $expr = new CExpr('owned', sprintf( + 'newNodeCtor("%s", %s, %s)', + $this->cAlias($srcName), + $attrsExpr->code, + implode(', ', $propArgs), + )); + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + + /** + * @param list $ctorParams + * @param list $args + */ + private function buildSlotsNew(string $srcName, string $fqcn, array $ctorParams, array $args): CExpr + { + $this->assertTrivialCtorOrForced($fqcn, $srcName); + if (count($args) > count($ctorParams)) { + $this->fail(sprintf('new %s with more args than constructor parameters', $srcName)); + } + $pre = []; + $post = []; + $attrsCode = null; + $propArgs = []; + foreach ($ctorParams as $i => $param) { + if ($param->getName() === 'attributes') { + if (!isset($args[$i])) { + $this->fail(sprintf('new %s does not pass $attributes', $srcName)); + } + if ($args[$i]->kind !== 'attrs') { + $this->fail(sprintf('new %s: $attributes argument is not an attributes expression', $srcName)); + } + foreach ($args[$i]->pre as $line) { + $pre[] = $line; + } + foreach ($args[$i]->post as $line) { + $post[] = $line; + } + $attrsCode = $args[$i]->code; + continue; + } + if (isset($args[$i])) { + if ($args[$i]->kind === 'subarr') { + $this->fail(sprintf('new %s: subNodes-style array argument outside ctor mode', $srcName)); + } + $propArgs[] = $this->toBorrowedCollect($args[$i], 'constructor argument', $pre, $post); + continue; + } + // omitted trailing parameter: materialize its reflected default + $propArgs[] = $this->defaultPropArg($param, $srcName); + } + if ($attrsCode === null) { + $this->fail(sprintf('new %s: constructor has no $attributes parameter', $srcName)); + } + if ($propArgs === []) { + $expr = new CExpr('owned', sprintf('newNode("%s", %s)', $this->cAlias($srcName), $attrsCode)); + } else { + $expr = new CExpr('owned', sprintf( + 'newNode("%s", %s, %s)', + $this->cAlias($srcName), + $attrsCode, + implode(', ', $propArgs), + )); + } + $expr->pre = $pre; + $expr->post = $post; + return $expr; + } + + private function defaultPropArg(ReflectionParameter $param, string $srcName): string + { + if (!$param->isDefaultValueAvailable()) { + $this->fail(sprintf('new %s omits parameter $%s which has no default', $srcName, $param->getName())); + } + $default = $param->getDefaultValue(); + $comment = sprintf('/* ctor default for omitted $%s */', $param->getName()); + if ($default === null) { + return 'nullptr ' . $comment; + } + if (is_bool($default)) { + return sprintf('zv::Val::boolean(%s) %s', $default ? 'true' : 'false', $comment); + } + if (is_int($default)) { + return sprintf('zv::Val::integer(%d) %s', $default, $comment); + } + if (is_array($default) && $default === []) { + return sprintf('zv::Arr::empty() %s', $comment); + } + $this->fail(sprintf('new %s omits parameter $%s with unsupported default', $srcName, $param->getName())); + return ''; + } + + /** @return list */ + private function reflectCtorParams(string $fqcn, string $srcName): array + { + if (!class_exists($fqcn)) { + $this->fail(sprintf('class %s (%s) does not exist', $srcName, $fqcn)); + } + $ctor = (new ReflectionClass($fqcn))->getConstructor(); + if ($ctor === null) { + $this->fail(sprintf('class %s has no constructor', $srcName)); + } + return $ctor->getParameters(); + } + + private function assertTrivialCtorOrForced(string $fqcn, string $srcName): void + { + if (isset(self::FORCED_SLOT_CLASSES[$fqcn])) { + return; + } + if (!isset(self::$ctorTriviality[$fqcn])) { + self::$ctorTriviality[$fqcn] = ['trivial' => $this->ctorIsTrivial($fqcn)]; + } + if (!self::$ctorTriviality[$fqcn]['trivial']) { + $this->fail(sprintf( + 'new %s: constructor of %s is not a trivial property-assignment ctor; add it to CTOR_CLASSES (PN_NEW_CTOR) or FORCED_SLOT_CLASSES with a justification', + $srcName, + $fqcn, + )); + } + } + + /** + * A ctor is trivial when its body consists only of `$this->x = $x;` + * statements — then writing property slots directly (PN_NEW) is exactly + * equivalent to calling it. + */ + private function ctorIsTrivial(string $fqcn): bool + { + $ctor = (new ReflectionClass($fqcn))->getConstructor(); + if ($ctor === null || $ctor->getFileName() === false) { + return false; + } + $lines = file($ctor->getFileName()); + if ($lines === false) { + return false; + } + $src = implode('', array_slice($lines, $ctor->getStartLine() - 1, $ctor->getEndLine() - $ctor->getStartLine() + 1)); + $open = strpos($src, '{'); + $close = strrpos($src, '}'); + if ($open === false || $close === false || $close < $open) { + return false; + } + $body = substr($src, $open + 1, $close - $open - 1); + $body = preg_replace('~/\*.*?\*/~s', '', $body); + $body = preg_replace('~//[^\n]*~', '', $body); + return preg_match('~^(?:\s*\$this->(\w+)\s*=\s*\$(\1)\s*;)*\s*$~', $body) === 1; + } + + private function parseClassName(): string + { + $t = $this->toks->peek(); + if (is_array($t) && in_array($t[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) { + $this->toks->next(); + $name = ltrim($t[1], '\\'); + // PHP < 8 tokenizes qualified names as T_STRING/T_NS_SEPARATOR sequences + while ($this->toks->is(T_NS_SEPARATOR)) { + $this->toks->next(); + $name .= '\\' . $this->toks->expectIdent(); + } + return $name; + } + $this->fail('expected a class name'); + return ''; + } + + private function resolveFqcn(string $srcName): string + { + $parts = explode('\\', $srcName); + $first = $parts[0]; + if (!isset($this->imports[$first])) { + $this->fail(sprintf('class name %s does not resolve through Php8.php\'s use statements', $srcName)); + } + $parts[0] = $this->imports[$first]; + return implode('\\', $parts); + } + + /** pn_class_resolve tries PhpParser\Node\, then PhpParser\: the source name works as-is. */ + private function cAlias(string $srcName): string + { + return str_replace('\\', '\\\\', $srcName); + } + + // ===== conversions ===== + + private function toOwned(CExpr $expr, string $context): string + { + switch ($expr->kind) { + case 'slot': + return sprintf('zv::Val::copyOf(%s)', $expr->code); + case 'owned': + case 'attrs': + return $expr->code; + case 'long': + return sprintf('zv::Val::integer(%s)', $expr->code); + case 'bool': + return sprintf('zv::Val::boolean(%s)', $expr->code); + case 'null': + return 'zv::Val::null()'; + case 'cstr': + return sprintf('zv::Val::string("%s", %d)', $expr->code, $expr->valueLen); + default: + $this->fail(sprintf('cannot convert kind %s to an owned zv::Val (%s)', $expr->kind, $context)); + return ''; + } + } + + private function toLong(CExpr $expr): string + { + if ($expr->kind === 'slot') { + return sprintf('%s.toLong()', $expr->code); + } + if ($expr->kind === 'long') { + return $expr->code; + } + $this->fail('cannot convert kind ' . $expr->kind . ' to zend_long'); + return ''; + } + + /** zv::Ref conversion for simple contexts that admit no temporaries */ + private function toZvpSimple(CExpr $expr, string $context): string + { + if ($expr->kind !== 'slot' || $expr->pre !== [] || $expr->post !== []) { + $this->fail(sprintf('%s operand must be a simple borrowed zv::Ref, got kind %s', $context, $expr->kind)); + } + return $expr->code; + } + + /** + * Borrowed-argument conversion for newNode/newNodeCtor props and arrayOf + * elements: slots pass through, scalars become inline owned temporaries + * (released at the end of the statement, after the callee addref'd them), + * node-building results become named zv::Val locals (RAII release). + * + * @param list $pre + * @param list $post + */ + private function toBorrowedCollect(CExpr $expr, string $context, array &$pre, array &$post, bool $nullAsPtr = true): string + { + foreach ($expr->pre as $line) { + $pre[] = $line; + } + switch ($expr->kind) { + case 'slot': + case 'subarr': + foreach ($expr->post as $line) { + $post[] = $line; + } + return $expr->code; + case 'null': + return $nullAsPtr ? 'nullptr' : 'zv::Val::null()'; + case 'owned': + case 'attrs': + $tmp = $this->tmp(); + $pre[] = sprintf('zv::Val %s = %s;', $tmp, $expr->code); + foreach ($expr->post as $line) { + $pre[] = $line; + } + return $tmp; + case 'long': + return sprintf('zv::Val::integer(%s)', $expr->code); + case 'bool': + return sprintf('zv::Val::boolean(%s)', $expr->code); + case 'cstr': + return sprintf('zv::Val::string("%s", %d)', $expr->code, $expr->valueLen); + default: + $this->fail(sprintf('cannot pass kind %s as a borrowed argument (%s)', $expr->kind, $context)); + return ''; + } + } + + /** + * zv::Ref conversion for helper-method arguments; owned values are + * materialized into named zv::Val locals and passed as .ref(). + * + * @param list $pre + * @param list $post + */ + private function toRefCollect(CExpr $expr, string $context, array &$pre, array &$post): string + { + foreach ($expr->pre as $line) { + $pre[] = $line; + } + switch ($expr->kind) { + case 'slot': + foreach ($expr->post as $line) { + $post[] = $line; + } + return $expr->code; + case 'owned': + case 'attrs': + $tmp = $this->tmp(); + $pre[] = sprintf('zv::Val %s = %s;', $tmp, $expr->code); + foreach ($expr->post as $line) { + $pre[] = $line; + } + return $tmp . '.ref()'; + case 'cstr': + $tmp = $this->tmp(); + $pre[] = sprintf('zv::Val %s = zv::Val::string("%s", %d);', $tmp, $expr->code, $expr->valueLen); + return $tmp . '.ref()'; + default: + $this->fail(sprintf('cannot pass kind %s as zv::Ref (%s)', $expr->kind, $context)); + return ''; + } + } + + /** Argument conversion for void helper calls (no temporaries allowed). */ + private function convertArg(CExpr $expr, string $spec, string $method): string + { + if ($expr->pre !== [] || $expr->post !== []) { + $this->fail(sprintf('%s() argument requires temporaries', $method)); + } + return $this->convertArgSpec($expr, $spec, $method); + } + + /** + * Argument conversion for value helper calls (temporaries collected). + * + * @param list $pre + * @param list $post + */ + private function convertArgCollect(CExpr $expr, string $spec, string $method, array &$pre, array &$post): string + { + if ($spec === 'zvp') { + return $this->toRefCollect($expr, $method . '() argument', $pre, $post); + } + foreach ($expr->pre as $line) { + $pre[] = $line; + } + foreach ($expr->post as $line) { + $post[] = $line; + } + return $this->convertArgSpec($expr, $spec, $method); + } + + private function convertArgSpec(CExpr $expr, string $spec, string $method): string + { + switch ($spec) { + case 'zvp': + if ($expr->kind !== 'slot') { + $this->fail(sprintf('%s() argument must be a borrowed zv::Ref, got %s', $method, $expr->kind)); + } + return $expr->code; + case 'attrs': + if ($expr->kind !== 'attrs') { + $this->fail(sprintf('%s() argument must be an attributes expression, got %s', $method, $expr->kind)); + } + return $expr->code; + case 'pos': + if ($expr->kind === 'tokpos') { + return $expr->code; + } + if ($expr->kind !== 'pos' && $expr->kind !== 'long') { + $this->fail(sprintf('%s() argument must be a position, got %s', $method, $expr->kind)); + } + return $expr->code; + case 'long': + return $this->toLong($expr); + case 'bool': + if ($expr->kind !== 'bool') { + $this->fail(sprintf('%s() argument must be a bool, got %s', $method, $expr->kind)); + } + return $expr->code; + default: + $this->fail('unknown arg spec ' . $spec); + return ''; + } + } + + // ===== small utilities ===== + + private function ind(int $level): string + { + return str_repeat("\t", $level); + } + + /** + * @param list $lines + * @return list + */ + private function indentAll(array $lines, int $indent): array + { + $out = []; + foreach ($lines as $line) { + $out[] = $this->ind($indent) . $line; + } + return $out; + } + + private function phpStringValue(string $literal): string + { + $quote = $literal[0]; + $inner = substr($literal, 1, -1); + if ($quote === "'") { + return str_replace(['\\\\', "\\'"], ['\\', "'"], $inner); + } + if (strpos($inner, '\\') !== false || strpos($inner, '$') !== false) { + $this->fail('unsupported double-quoted string literal: ' . $literal); + } + return $inner; + } + + private function cEscape(string $value): string + { + if (preg_match('/^[\x20-\x7e]*$/', $value) !== 1) { + $this->fail('string literal contains non-printable characters'); + } + return str_replace(['\\', '"'], ['\\\\', '\\"'], $value); + } + +} + +// ===== extraction ===== + +/** + * @return array{entries: array, imports: array} + */ +function extractReduceCallbacks(string $php8Path): array +{ + $src = file_get_contents($php8Path); + if ($src === false) { + fwrite(STDERR, "cannot read $php8Path\n"); + exit(1); + } + $tokens = token_get_all($src); + $n = count($tokens); + + // parse the use statements (Error, Modifiers, Node, Expr, Name, Scalar, Stmt, ...) + $imports = []; + for ($i = 0; $i < $n; $i++) { + $t = $tokens[$i]; + if (!is_array($t) || $t[0] !== T_USE) { + continue; + } + $fqcn = ''; + for ($j = $i + 1; $j < $n; $j++) { + $u = $tokens[$j]; + if (is_array($u) && in_array($u[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) { + $fqcn .= $u[1]; + } elseif (is_array($u) && $u[0] === T_WHITESPACE) { + continue; + } elseif ($u === ';') { + break; + } else { + $fqcn = ''; // aliased or grouped use — not present in Php8.php + break; + } + } + if ($fqcn !== '') { + $parts = explode('\\', trim($fqcn, '\\')); + $imports[end($parts)] = trim($fqcn, '\\'); + } + } + + // locate the initReduceCallbacks method body + $start = null; + for ($i = 0; $i < $n; $i++) { + $t = $tokens[$i]; + if (is_array($t) && $t[0] === T_STRING && $t[1] === 'initReduceCallbacks') { + $start = $i; + break; + } + } + if ($start === null) { + fwrite(STDERR, "initReduceCallbacks not found in $php8Path\n"); + exit(1); + } + $i = $start; + while ($i < $n && $tokens[$i] !== '{') { + $i++; + } + $depth = 1; + $i++; + + $entries = []; + while ($i < $n && $depth > 0) { + $t = $tokens[$i]; + if ($t === '{') { + $depth++; + $i++; + continue; + } + if ($t === '}') { + $depth--; + $i++; + continue; + } + if (is_array($t) && $t[0] === T_LNUMBER) { + $rule = (int) $t[1]; + $j = $i + 1; + while ($j < $n && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT], true)) { + $j++; + } + if (!is_array($tokens[$j]) || $tokens[$j][0] !== T_DOUBLE_ARROW) { + $i++; + continue; + } + $j++; + while ($j < $n && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT], true)) { + $j++; + } + if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING && strtolower($tokens[$j][1]) === 'null') { + $entries[$rule] = null; + $i = $j + 1; + continue; + } + // static function ($self, $stackPos) { BODY } + while ($j < $n && $tokens[$j] !== '{') { + $j++; + } + $bodyStart = $j + 1; + $d = 1; + $k = $bodyStart; + while ($k < $n && $d > 0) { + $u = $tokens[$k]; + if ($u === '{' || (is_array($u) && in_array($u[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true))) { + $d++; + } elseif ($u === '}') { + $d--; + if ($d === 0) { + break; + } + } + $k++; + } + $body = ''; + for ($p = $bodyStart; $p < $k; $p++) { + $body .= is_array($tokens[$p]) ? $tokens[$p][1] : $tokens[$p]; + } + $entries[$rule] = trim($body); + $i = $k + 1; + continue; + } + $i++; + } + ksort($entries); + return ['entries' => $entries, 'imports' => $imports]; +} + +function normalizeBody(string $body): string +{ + return preg_replace('/\s+/', ' ', trim($body)); +} + +// ===== helper definitions included per generated file when referenced ===== + +/** @return list */ +function helperDefinitions(): array +{ + return [ + [ + 'needle' => 'phpArrayPop', + 'code' => "/* array_pop(\$this->semValue) on a packed 0..n-1 list, with the same\n" + . " * copy-on-write separation PHP performs on write. Also resets\n" + . " * nNextFreeElement like array_pop. */\n" + . "static void phpArrayPop(zval *arr)\n" + . "{\n" + . "\tSEPARATE_ARRAY(arr);\n" + . "\tHashTable *ht = Z_ARRVAL_P(arr);\n" + . "\tuint32_t n = zend_hash_num_elements(ht);\n" + . "\tif (n == 0) {\n" + . "\t\treturn;\n" + . "\t}\n" + . "\tzend_hash_index_del(ht, n - 1);\n" + . "\tht->nNextFreeElement = (zend_long) (n - 1);\n" + . "\tzend_hash_internal_pointer_reset(ht);\n" + . "}\n", + ], + ]; +} + +// ===== main ===== + +$php8Path = $root . '/' . PHP8_RELATIVE; +$overridesDir = __DIR__ . '/../src/parser/action-overrides'; +$parserDir = __DIR__ . '/../src/parser'; + +$extracted = extractReduceCallbacks($php8Path); +$entries = $extracted['entries']; +$imports = $extracted['imports']; + +if (count($entries) === 0 || $imports === []) { + fwrite(STDERR, "extraction failed: no entries or no use statements found\n"); + exit(1); +} + +$transpiler = new Transpiler($imports); + +/** @var array, override: string|null}> $cases */ +$cases = []; +/** @var list $failures */ +$failures = []; +/** @var array $usedOverrides */ +$usedOverrides = []; + +foreach ($entries as $rule => $body) { + if ($body === null) { + continue; + } + $norm = normalizeBody($body); + $sha1 = sha1($norm); + $overrideFile = $overridesDir . '/' . $sha1 . '.inc'; + if (is_file($overrideFile)) { + $content = rtrim(file_get_contents($overrideFile), "\n"); + $cases[$rule] = ['lines' => explode("\n", $content), 'override' => $sha1]; + $usedOverrides[$sha1] = true; + continue; + } + try { + $cases[$rule] = ['lines' => $transpiler->transpile($body), 'override' => null]; + } catch (TranspileFailure $e) { + $failures[] = ['rule' => $rule, 'sha1' => $sha1, 'body' => $norm, 'reason' => $e->getMessage()]; + } +} + +if ($failures !== []) { + fwrite(STDERR, sprintf( + "generate-parser-actions: %d closure(s) could not be transpiled and have no override.\n" + . "Port each one to C++ by hand (the existing generated cases are the cookbook) and save it as\n" + . "%s/.inc, then re-run.\n\n", + count($failures), + 'turbo-ext/src/parser/action-overrides', + )); + foreach ($failures as $failure) { + fwrite(STDERR, sprintf( + "rule %d\n sha1: %s\n reason: %s\n body: %s\n\n", + $failure['rule'], + $failure['sha1'], + $failure['reason'], + $failure['body'], + )); + } + exit(1); +} + +// warn about orphaned overrides (upstream body changed or vanished) +$orphans = []; +foreach (glob($overridesDir . '/*.inc') ?: [] as $file) { + $sha1 = basename($file, '.inc'); + if (!isset($usedOverrides[$sha1])) { + $orphans[] = basename($file); + } +} +if ($orphans !== []) { + fwrite(STDERR, "warning: unused override file(s) — the upstream body they matched no longer exists:\n"); + foreach ($orphans as $orphan) { + fwrite(STDERR, " action-overrides/$orphan\n"); + } +} + +// ===== split into three roughly equal-by-rule-count files ===== + +$ruleNumbers = array_keys($cases); +sort($ruleNumbers); +$total = count($ruleNumbers); +$perFile = (int) ceil($total / 3); +$fileRules = [ + array_slice($ruleNumbers, 0, $perFile), + array_slice($ruleNumbers, $perFile, $perFile), + array_slice($ruleNumbers, 2 * $perFile), +]; +$split1 = $fileRules[1][0]; +$split2 = $fileRules[2][0]; + +$generatedBy = '/* GENERATED by turbo-ext/bin/generate-parser-actions.php — do not edit */'; + +$outputs = []; +foreach ($fileRules as $idx => $rules) { + $fileNo = $idx + 1; + $first = $rules[0]; + $last = $rules[count($rules) - 1]; + + $body = ''; + foreach ($rules as $rule) { + $case = $cases[$rule]; + if ($case['override'] !== null) { + $body .= sprintf("\t/* rule %d (from action-overrides/%s.inc) */\n", $rule, $case['override']); + } else { + $body .= sprintf("\t/* rule %d */\n", $rule); + } + $body .= sprintf("\tcase %d: {\n", $rule); + foreach ($case['lines'] as $line) { + $body .= rtrim($line) === '' ? "\n" : $line . "\n"; + } + $body .= "\t\treturn true;\n"; + $body .= "\t}\n"; + } + + $helpers = ''; + foreach (helperDefinitions() as $helper) { + if (preg_match('/\b' . preg_quote($helper['needle'], '/') . '\b/', $body) === 1) { + $helpers .= "\n" . $helper['code']; + } + } + + $content = $generatedBy . "\n"; + $content .= "/*\n"; + $content .= sprintf(" * Reduce actions for rules %d-%d of php-parser's Php8 grammar\n", $first, $last); + $content .= " * (vendor/nikic/php-parser/lib/PhpParser/Parser/Php8.php, initReduceCallbacks).\n"; + $content .= " * Rules with a null callback have no case here; returning false makes the\n"; + $content .= " * engine apply the default action. Hand-ported special cases are emitted\n"; + $content .= " * verbatim from src/parser/action-overrides/.inc.\n"; + $content .= " */\n"; + $content .= "\n#include \"ParserEngine.h\"\n"; + $content .= "\nnamespace phpstanturbo {\n"; + $content .= $helpers; + $content .= "\n"; + $content .= sprintf("bool ParserEngine::reduceRange%d(int rule, int stackPos)\n", $fileNo); + $content .= "{\n"; + $content .= "\tswitch (rule) {\n"; + $content .= $body; + $content .= "\tdefault:\n"; + $content .= "\t\treturn false;\n"; + $content .= "\t}\n"; + $content .= "}\n"; + $content .= "\n} // namespace phpstanturbo\n"; + + $outputs[sprintf('%s/ParserRunnerActions%d.cpp', $parserDir, $fileNo)] = $content; +} + +$splitHeader = $generatedBy . "\n"; +$splitHeader .= "/* Dispatch boundaries for ParserEngine::reduce() (ParserRunner.cpp): rules\n"; +$splitHeader .= " * below PN_REDUCE_SPLIT_1 live in ParserRunnerActions1.cpp (reduceRange1),\n"; +$splitHeader .= " * below PN_REDUCE_SPLIT_2 in ParserRunnerActions2.cpp (reduceRange2), the\n"; +$splitHeader .= " * rest in ParserRunnerActions3.cpp (reduceRange3). */\n"; +$splitHeader .= "\n"; +$splitHeader .= "#ifndef PHPSTANTURBO_PN_ACTIONS_SPLIT_H\n"; +$splitHeader .= "#define PHPSTANTURBO_PN_ACTIONS_SPLIT_H\n"; +$splitHeader .= "\n"; +$splitHeader .= sprintf("#define PN_REDUCE_SPLIT_1 %d\n", $split1); +$splitHeader .= sprintf("#define PN_REDUCE_SPLIT_2 %d\n", $split2); +$splitHeader .= "\n"; +$splitHeader .= "#endif\n"; +$outputs[$parserDir . '/ParserRunnerActionsSplit.h'] = $splitHeader; + +foreach ($outputs as $path => $content) { + if (is_file($path) && file_get_contents($path) === $content) { + continue; + } + file_put_contents($path, $content); +} + +$overrideRules = []; +foreach ($cases as $rule => $case) { + if ($case['override'] !== null) { + $overrideRules[] = $rule; + } +} + +printf( + "generated %d cases (%d transpiled, %d from overrides: rules %s) across 3 files; split at %d / %d\n", + count($cases), + count($cases) - count($overrideRules), + count($overrideRules), + implode(', ', $overrideRules), + $split1, + $split2, +); diff --git a/turbo-ext/bin/side-by-side.php b/turbo-ext/bin/side-by-side.php new file mode 100644 index 00000000000..991577eba7d --- /dev/null +++ b/turbo-ext/bin/side-by-side.php @@ -0,0 +1,495 @@ += 8.0 without vendor/. + */ + +error_reporting(E_ALL); + +$root = dirname(__DIR__, 2); +chdir($root); + +$manifestFile = 'turbo-ext/shadowed-classes.json'; +$manifest = json_decode(file_get_contents($manifestFile), true, 8, JSON_THROW_ON_ERROR); + +/** + * @return array + * methods of the first class in the file, in source order + */ +function parsePhpMethods(string $file): array +{ + $tokens = token_get_all(file_get_contents($file)); + $methods = []; + + $line = 1; + $depth = 0; + $awaitingClassBrace = false; + $inClass = false; + $classDepth = 0; + + $declStartLine = null; + $visibility = 'public'; + $static = false; + $pendingMethod = null; // [name, visibility, static, startLine] + $inMethodBody = false; + $prevSignificant = null; + + foreach ($tokens as $token) { + if (is_array($token)) { + [$id, $text, $line] = $token; + } else { + $id = null; + $text = $token; + } + + if ($id === T_WHITESPACE || $id === T_COMMENT) { + $line += substr_count($text, "\n"); + continue; + } + + if ($id === T_CLASS && $prevSignificant !== T_DOUBLE_COLON && $prevSignificant !== T_NEW && !$inClass) { + $awaitingClassBrace = true; + } + + if ($text === '{' || $id === T_CURLY_OPEN || $id === T_DOLLAR_OPEN_CURLY_BRACES) { + $depth++; + if ($awaitingClassBrace) { + $inClass = true; + $classDepth = $depth; + $awaitingClassBrace = false; + } elseif ($pendingMethod !== null && !$inMethodBody && $depth === $classDepth + 1) { + $inMethodBody = true; + } + } elseif ($text === '}') { + $depth--; + if ($inMethodBody && $depth === $classDepth) { + $methods[$pendingMethod[0]] = [ + 'visibility' => $pendingMethod[1], + 'static' => $pendingMethod[2], + 'startLine' => $pendingMethod[3], + 'endLine' => $line, + ]; + $pendingMethod = null; + $inMethodBody = false; + $declStartLine = null; + $visibility = 'public'; + $static = false; + } elseif ($inClass && $depth < $classDepth) { + break; // first class only + } + } elseif ($inClass && !$inMethodBody && $depth === $classDepth) { + if ($id === T_DOC_COMMENT || $id === T_ATTRIBUTE || $id === T_FINAL || $id === T_ABSTRACT + || $id === T_PUBLIC || $id === T_PROTECTED || $id === T_PRIVATE || $id === T_STATIC + || (defined('T_READONLY') && $id === T_READONLY) || $id === T_FUNCTION || $id === T_CONST || $id === T_VAR + ) { + $declStartLine ??= $line; + } + if ($id === T_PUBLIC) { + $visibility = 'public'; + } elseif ($id === T_PROTECTED) { + $visibility = 'protected'; + } elseif ($id === T_PRIVATE) { + $visibility = 'private'; + } elseif ($id === T_STATIC) { + $static = true; + } elseif ($prevSignificant === T_FUNCTION && $id !== null && $text !== '(') { + // the method name — not necessarily T_STRING: names like + // and()/or() tokenize as T_LOGICAL_AND/T_LOGICAL_OR + $pendingMethod = [$text, $visibility, $static, $declStartLine ?? $line]; + } elseif ($text === ';') { + if ($pendingMethod !== null) { // abstract/interface method + $methods[$pendingMethod[0]] = [ + 'visibility' => $pendingMethod[1], + 'static' => $pendingMethod[2], + 'startLine' => $pendingMethod[3], + 'endLine' => $line, + ]; + $pendingMethod = null; + } + $declStartLine = null; + $visibility = 'public'; + $static = false; + } + } + + if ($id !== null) { + $prevSignificant = $text === '&' ? $prevSignificant : $id; // skip & in "function &name" + $line += substr_count($text, "\n"); + } else { + $prevSignificant = $text; + } + } + + return $methods; +} + +/** + * @return array + * PHP_METHOD implementations, in source order + */ +function parseCppMethods(string $file): array +{ + $lines = file($file); + $methods = []; + + // three anchor kinds, in preference order: the handle-class member that + // mirrors the PHP twin (logic-to-logic), the legacy PHP_METHOD glue, and + // the reg::Class registration site (glue lambda). Keyword-clashing names + // carry a trailing underscore natively (and_/or_). + $handleClassMembers = []; + if (preg_match('/^namespace phpstanturbo \{$/m', file_get_contents($file)) === 1) { + foreach ($lines as $i => $lineText) { + if (preg_match('/^\t(?:static\s+)?(?:[\w:<>]+(?:\s*[&*])?\s+)+(\w+?)(_?)\(/', $lineText, $hm) === 1 + && !str_starts_with(trim($lineText), 'return') + ) { + $handleClassMembers[$hm[1]] ??= $i; + } + } + } + + foreach ($lines as $i => $lineText) { + if ( + preg_match('/^\s*(?:static\s+)?PHP_METHOD\(\s*\w+\s*,\s*(\w+)\s*\)/', $lineText, $m) !== 1 + && preg_match('/^\s*(?:cls\.|\.)method\("(\w+)"/', $lineText, $m) !== 1 + ) { + continue; + } + // prefer the handle-class member of the same (or underscore-suffixed) name + if (isset($handleClassMembers[$m[1]])) { + $i = $handleClassMembers[$m[1]]; + $lineText = $lines[$i]; + } + + // include the contiguous comment block right above the method + $start = $i; + while ($start > 0) { + $prev = trim($lines[$start - 1]); + if ($prev === '' || (!str_starts_with($prev, '//') && !str_starts_with($prev, '/*') && !str_starts_with($prev, '*'))) { + break; + } + $start--; + } + + // find the matching closing brace, ignoring braces in strings/comments + $depth = 0; + $opened = false; + $end = $i; + $inBlockComment = false; + for ($j = $i; $j < count($lines); $j++) { + $text = $lines[$j]; + $len = strlen($text); + $inString = null; + for ($k = 0; $k < $len; $k++) { + $c = $text[$k]; + if ($inBlockComment) { + if ($c === '*' && ($text[$k + 1] ?? '') === '/') { + $inBlockComment = false; + $k++; + } + continue; + } + if ($inString !== null) { + if ($c === '\\') { + $k++; + } elseif ($c === $inString) { + $inString = null; + } + continue; + } + if ($c === '"' || $c === "'") { + $inString = $c; + } elseif ($c === '/' && ($text[$k + 1] ?? '') === '/') { + break; + } elseif ($c === '/' && ($text[$k + 1] ?? '') === '*') { + $inBlockComment = true; + $k++; + } elseif ($c === '{') { + $depth++; + $opened = true; + } elseif ($c === '}') { + $depth--; + } + } + if ($opened && $depth === 0) { + $end = $j; + break; + } + } + + $methods[$m[1]] = ['startLine' => $start + 1, 'endLine' => $end + 1]; + } + + return $methods; +} + +/** @return array{skipped: string|null, phpMethods: array, cppMethods: array, missingNative: list, orphanNative: list} */ +function analyzePair(string $className, array $entry): array +{ + if (!is_file($entry['php'])) { + if ($entry['vendored'] ?? false) { + return ['skipped' => sprintf('%s not present (vendor/ not installed)', $entry['php']), 'phpMethods' => [], 'cppMethods' => [], 'missingNative' => [], 'orphanNative' => []]; + } + throw new RuntimeException(sprintf('%s: PHP file %s does not exist', $className, $entry['php'])); + } + if (!is_file($entry['cpp'])) { + throw new RuntimeException(sprintf('%s: C++ file %s does not exist', $className, $entry['cpp'])); + } + + $phpMethods = parsePhpMethods($entry['php']); + $cppMethods = parseCppMethods($entry['cpp']); + + // Every public PHP method must exist natively — the stub subclass is + // empty, so a missing native method is a fatal when the extension is on. + $missingNative = []; + foreach ($phpMethods as $name => $info) { + if ($info['visibility'] === 'public' && !isset($cppMethods[$name])) { + $missingNative[] = $name; + } + } + + // Every native method must correspond to a PHP method (any visibility) — + // an orphan means the implementations drifted apart. + $orphanNative = []; + foreach ($cppMethods as $name => $info) { + if (!isset($phpMethods[$name])) { + $orphanNative[] = $name; + } + } + + return ['skipped' => null, 'phpMethods' => $phpMethods, 'cppMethods' => $cppMethods, 'missingNative' => $missingNative, 'orphanNative' => $orphanNative]; +} + +/** + * The manifest must stay complete: every stub, every enabler require_once and + * every per-class .cpp file must correspond to a manifest entry (and vice + * versa), and stubs must be empty shells — a shadowed class missing from the + * manifest would silently escape the parity and version-coupling checks. + * + * @return list problems + */ +function checkStructure(array $manifest): array +{ + $problems = []; + + $fromManifest = []; + foreach ($manifest as $className => $entry) { + $fromManifest[basename($entry['cpp'], '.cpp')] = $className; + } + + $sets = [ + 'turbo-ext/stubs/*.php' => array_map(static fn ($f) => basename($f, '.php'), glob('turbo-ext/stubs/*.php')), + 'require_once in TurboExtensionEnabler' => (static function (): array { + preg_match_all('~stubs/(\w+)\.php~', file_get_contents('src/Turbo/TurboExtensionEnabler.php'), $m); + return $m[1]; + })(), + 'class-defining .cpp files (PHP_METHOD or reg::Class)' => array_values(array_filter(array_map( + static fn ($f) => preg_match('~PHP_METHOD\(PHPStanTurbo_|reg::Class\s+\w+\("PHPStanTurbo~', file_get_contents($f)) === 1 ? basename($f, '.cpp') : null, + array_merge(glob('turbo-ext/src/*.cpp'), glob('turbo-ext/src/parser/*.cpp')), + ))), + ]; + foreach ($sets as $what => $names) { + foreach (array_diff($names, array_keys($fromManifest)) as $extra) { + $problems[] = sprintf('%s from %s has no entry in shadowed-classes.json', $extra, $what); + } + foreach (array_diff(array_keys($fromManifest), $names) as $missing) { + $problems[] = sprintf('manifest entry %s (%s) is missing from %s', $fromManifest[$missing], $missing, $what); + } + } + + // stubs must be empty final shells extending the matching native class + foreach (glob('turbo-ext/stubs/*.php') as $stubFile) { + $base = basename($stubFile, '.php'); + $src = file_get_contents($stubFile); + if (preg_match('~class\s+(\w+)\s+extends\s+\\\\PHPStanTurbo\\\\(\w+)\s*\{(.*)\}~s', $src, $m) !== 1) { + $problems[] = sprintf('%s does not declare "class X extends \PHPStanTurbo\X"', $stubFile); + continue; + } + if ($m[1] !== $base || $m[2] !== $base) { + $problems[] = sprintf('%s: class %s extends \PHPStanTurbo\%s — names must both match the file name', $stubFile, $m[1], $m[2]); + } + if (trim($m[3]) !== '') { + $problems[] = sprintf('%s: the stub body must be empty — a member declared here would exist only when the extension is loaded', $stubFile); + } + } + + return $problems; +} + +if (in_array('--update-manifest', $argv, true)) { + // Regenerate the manifest from ground truth: each stub names the shadowed + // class, the autoloader locates its PHP implementation (the enabler is NOT + // run, so class names resolve to the originals, not the stubs), and the + // native file is the same-named .cpp. Run this after adding a shadowed + // class; the CI checks then verify the committed manifest matches reality. + require $root . '/vendor/autoload.php'; + $entries = []; + foreach (glob('turbo-ext/stubs/*.php') as $stubFile) { + $src = file_get_contents($stubFile); + if (preg_match('~^namespace\s+([\w\\\\]+);~m', $src, $ns) !== 1 + || preg_match('~class\s+(\w+)\s+extends\s+\\\\PHPStanTurbo\\\\\w+~', $src, $cls) !== 1 + ) { + fwrite(STDERR, sprintf("cannot parse %s\n", $stubFile)); + exit(1); + } + $className = $ns[1] . '\\' . $cls[1]; + $phpFile = substr(realpath((new ReflectionClass($className))->getFileName()), strlen(realpath($root)) + 1); + $base = basename($stubFile, '.php'); + $cppFile = 'turbo-ext/src/' . $base . '.cpp'; + if (!is_file($cppFile)) { + $cppFile = 'turbo-ext/src/parser/' . $base . '.cpp'; + } + $entry = [ + 'php' => $phpFile, + 'cpp' => $cppFile, + ]; + if (str_starts_with($phpFile, 'vendor/')) { + $entry['vendored'] = true; + } + $entries[$className] = $entry; + } + ksort($entries); + file_put_contents($manifestFile, json_encode($entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"); + printf("wrote %s (%d classes)\n", $manifestFile, count($entries)); + exit(0); +} + +$check = in_array('--check', $argv, true); + +if ($check) { + $failed = false; + foreach (checkStructure($manifest) as $problem) { + printf("✗ %s\n", $problem); + $failed = true; + } + foreach ($manifest as $className => $entry) { + $result = analyzePair($className, $entry); + if ($result['skipped'] !== null) { + printf("~ %s: skipped, %s\n", $className, $result['skipped']); + continue; + } + if ($result['missingNative'] === [] && $result['orphanNative'] === []) { + printf("✓ %s: %d methods paired\n", $className, count($result['cppMethods'])); + continue; + } + $failed = true; + foreach ($result['missingNative'] as $name) { + printf("✗ %s::%s() is public in %s but has no PHP_METHOD in %s\n", $className, $name, $entry['php'], $entry['cpp']); + } + foreach ($result['orphanNative'] as $name) { + printf("✗ PHP_METHOD %s in %s has no counterpart method in %s\n", $name, $entry['cpp'], $entry['php']); + } + } + exit($failed ? 1 : 0); +} + +$output = $argv[1] ?? 'turbo-ext/side-by-side.html'; + +function codePane(string $file, int $startLine, int $endLine): string +{ + $lines = array_slice(file($file), $startLine - 1, $endLine - $startLine + 1); + $html = ''; + foreach ($lines as $i => $text) { + $html .= sprintf("%4d%s\n", $startLine + $i, htmlspecialchars(rtrim($text, "\n"), ENT_QUOTES)); + } + + return sprintf('
%s
', $html); +} + +$gitHead = trim((string) shell_exec('git log -1 --format=%h 2>/dev/null')); +$body = ''; +$toc = ''; + +foreach ($manifest as $className => $entry) { + $result = analyzePair($className, $entry); + $anchor = strtolower(str_replace('\\', '-', $className)); + $toc .= sprintf('
  • %s
  • ', $anchor, htmlspecialchars($className)); + + $body .= sprintf( + '

    %s

    PHP %s   C++ %s%s

    ', + $anchor, + htmlspecialchars($className), + htmlspecialchars($entry['php']), + htmlspecialchars($entry['cpp']), + ($entry['vendored'] ?? false) ? ' (PHP side is vendored — pinned by composer.lock)' : '', + ); + + if ($result['skipped'] !== null) { + $body .= sprintf('

    Skipped: %s

    ', htmlspecialchars($result['skipped'])); + continue; + } + + foreach ($result['phpMethods'] as $name => $info) { + $native = $result['cppMethods'][$name] ?? null; + $label = sprintf( + '%s%s%s()', + $info['visibility'] === 'public' ? '' : $info['visibility'] . ' ', + $info['static'] ? 'static ' : '', + $name, + ); + if ($native === null) { + if ($info['visibility'] === 'public') { + $body .= sprintf('

    %s no native counterpart!

    ', htmlspecialchars($label)); + $body .= sprintf('
    %s
    missing
    ', codePane($entry['php'], $info['startLine'], $info['endLine'])); + } else { + $body .= sprintf('

    %s PHP-only helper (native side inlines or reimplements it)

    ', htmlspecialchars($label)); + } + continue; + } + $body .= sprintf('

    %s

    %s
    %s
    ', htmlspecialchars($label), codePane($entry['php'], $info['startLine'], $info['endLine']), codePane($entry['cpp'], $native['startLine'], $native['endLine'])); + } + + foreach ($result['orphanNative'] as $name) { + $native = $result['cppMethods'][$name]; + $body .= sprintf('

    %s() native only — no PHP counterpart!

    ', htmlspecialchars($name)); + $body .= sprintf('
    missing
    %s
    ', codePane($entry['cpp'], $native['startLine'], $native['endLine'])); + } + + $body .= ''; +} + +$html = << +phpstan_turbo — PHP ↔ C++ side by side + +

    phpstan_turbo — shadowed classes, PHP ↔ C++

    +

    Generated from turbo-ext/shadowed-classes.json at commit {$gitHead} +by php turbo-ext/bin/side-by-side.php. Left: the PHP implementation +(used when the extension is not loaded). Right: the native implementation the +stub shadows it with.

    +
      {$toc}
    +{$body} +HTML; + +file_put_contents($output, $html); +printf("wrote %s (%d classes)\n", $output, count($manifest)); diff --git a/turbo-ext/composer.json b/turbo-ext/composer.json deleted file mode 100644 index 3a4551828d2..00000000000 --- a/turbo-ext/composer.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require": { - "phalcon/zephir": "^0.19.0" - } -} diff --git a/turbo-ext/composer.lock b/turbo-ext/composer.lock deleted file mode 100644 index 80373698190..00000000000 --- a/turbo-ext/composer.lock +++ /dev/null @@ -1,335 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "2f885be27ef2d5ca3a9dd4cd644f20f0", - "packages": [ - { - "name": "monolog/monolog", - "version": "2.10.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" - }, - "provide": { - "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" - }, - "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "elasticsearch/elasticsearch": "^7 || ^8", - "ext-json": "*", - "graylog2/gelf-php": "^1.4.2 || ^2@dev", - "guzzlehttp/guzzle": "^7.4", - "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", - "php-amqplib/php-amqplib": "~2.4 || ^3", - "phpspec/prophecy": "^1.15", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.38 || ^9.6.19", - "predis/predis": "^1.1 || ^2.0", - "rollbar/rollbar": "^1.3 || ^2 || ^3", - "ruflin/elastica": "^7", - "swiftmailer/swiftmailer": "^5.3|^6.0", - "symfony/mailer": "^5.4 || ^6", - "symfony/mime": "^5.4 || ^6" - }, - "suggest": { - "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", - "doctrine/couchdb": "Allow sending log messages to a CouchDB server", - "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", - "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", - "ext-mbstring": "Allow to work properly with unicode symbols", - "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", - "ext-openssl": "Required to send log messages using SSL", - "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", - "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Monolog\\": "src/Monolog" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "https://seld.be" - } - ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "https://github.com/Seldaek/monolog", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "support": { - "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.10.0" - }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } - ], - "time": "2024-11-12T12:43:37+00:00" - }, - { - "name": "phalcon/cli-options-parser", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/phalcon/cli-options-parser.git", - "reference": "ec4d4cd0b7e61046b88957a4028ad01dfa14f4e6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phalcon/cli-options-parser/zipball/ec4d4cd0b7e61046b88957a4028ad01dfa14f4e6", - "reference": "ec4d4cd0b7e61046b88957a4028ad01dfa14f4e6", - "shasum": "" - }, - "require": { - "php": ">=8.0" - }, - "require-dev": { - "pds/skeleton": "^1.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "Phalcon\\Cop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Phalcon Team", - "email": "team@phalconphp.com", - "homepage": "https://phalconphp.com/en/team" - }, - { - "name": "Contributors", - "homepage": "https://github.com/phalcon/cli-options-parser/graphs/contributors" - } - ], - "description": "Command line arguments/options parser.", - "homepage": "https://phalconphp.com", - "keywords": [ - "argparse", - "cli", - "command", - "command-line", - "getopt", - "line", - "option", - "optparse", - "parser", - "terminal" - ], - "support": { - "discord": "https://phalcon.io/discord/", - "issues": "https://github.com/phalcon/cli-options-parser/issues", - "source": "https://github.com/phalcon/cli-options-parser" - }, - "funding": [ - { - "url": "https://github.com/phalcon", - "type": "github" - }, - { - "url": "https://opencollective.com/phalcon", - "type": "open_collective" - } - ], - "time": "2023-11-24T16:04:00+00:00" - }, - { - "name": "phalcon/zephir", - "version": "0.19.0", - "source": { - "type": "git", - "url": "https://github.com/zephir-lang/zephir.git", - "reference": "ab31770196ab2aab5b9b62860e296da8a70603c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zephir-lang/zephir/zipball/ab31770196ab2aab5b9b62860e296da8a70603c6", - "reference": "ab31770196ab2aab5b9b62860e296da8a70603c6", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-hash": "*", - "ext-mbstring": "*", - "ext-pcre": "*", - "ext-xml": "*", - "ext-zlib": "*", - "monolog/monolog": "^2.3", - "phalcon/cli-options-parser": "^2.0", - "php": ">=8.0" - }, - "require-dev": { - "ext-gmp": "*", - "ext-pdo": "*", - "ext-pdo_sqlite": "*", - "ext-zip": "*", - "phpunit/phpunit": "^9.5", - "psr/log": "1.1.*", - "squizlabs/php_codesniffer": "^3.7" - }, - "bin": [ - "zephir" - ], - "type": "library", - "autoload": { - "psr-4": { - "Zephir\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phalcon Team", - "email": "team@zephir-lang.com", - "homepage": "https://zephir-lang.com" - }, - { - "name": "Contributors", - "homepage": "https://github.com/zephir-lang/zephir/graphs/contributors" - } - ], - "description": "Zephir is a compiled high level language aimed to the creation of C-extensions for PHP", - "homepage": "https://zephir-lang.com", - "keywords": [ - "extension", - "internals", - "phalcon", - "zephir" - ], - "support": { - "docs": "https://docs.zephir-lang.com", - "irc": "irc://irc.freenode.net/zephir", - "issues": "https://github.com/zephir-lang/zephir/issues?state=open", - "source": "https://github.com/zephir-lang/zephir" - }, - "funding": [ - { - "url": "https://opencollective.com/phalcon", - "type": "custom" - }, - { - "url": "https://github.com/phalcon", - "type": "github" - } - ], - "time": "2025-05-13T11:35:31+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.9.0" -} diff --git a/turbo-ext/config.json b/turbo-ext/config.json deleted file mode 100644 index 03eed73fec8..00000000000 --- a/turbo-ext/config.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "stubs": { - "path": "ide\/%version%\/%namespace%\/", - "stubs-run-after-generate": false, - "banner": "" - }, - "api": { - "path": "doc\/%version%", - "theme": { - "name": "zephir", - "options": { - "github": null, - "analytics": null, - "main_color": "#3E6496", - "link_color": "#3E6496", - "link_hover_color": "#5F9AE7" - } - } - }, - "warnings": { - "unused-variable": true, - "unused-variable-external": false, - "possible-wrong-parameter": true, - "possible-wrong-parameter-undefined": false, - "nonexistent-function": true, - "nonexistent-class": true, - "non-valid-isset": true, - "non-array-update": true, - "non-valid-objectupdate": true, - "non-valid-fetch": true, - "invalid-array-index": true, - "non-array-append": true, - "invalid-return-type": true, - "unreachable-code": true, - "nonexistent-constant": true, - "not-supported-magic-constant": true, - "non-valid-decrement": true, - "non-valid-increment": true, - "non-valid-clone": true, - "non-valid-new": true, - "non-array-access": true, - "invalid-reference": true, - "invalid-typeof-comparison": true, - "conditional-initialization": true - }, - "optimizations": { - "static-type-inference": true, - "static-type-inference-second-pass": true, - "local-context-pass": true, - "constant-folding": true, - "static-constant-class-folding": true, - "call-gatherer-pass": true, - "check-invalid-reads": false, - "internal-call-transformation": false - }, - "extra": { - "indent": "spaces", - "export-classes": false - }, - "namespace": "phpstanturbo", - "name": "phpstan_turbo", - "description": "", - "author": "PHPStan", - "version": "0.0.1", - "verbose": false, - "requires": { - "extensions": [] - } -} diff --git a/turbo-ext/patches/php-cpp-base-count-int64.patch b/turbo-ext/patches/php-cpp-base-count-int64.patch new file mode 100644 index 00000000000..5cbd64702ef --- /dev/null +++ b/turbo-ext/patches/php-cpp-base-count-int64.patch @@ -0,0 +1,15 @@ +diff --git a/zend/base.cpp b/zend/base.cpp +index 8199f52..3ee8ef3 100644 +--- a/zend/base.cpp ++++ b/zend/base.cpp +@@ -277,7 +277,9 @@ Php::Value Base::__count(Php::Parameters ¶ms) + if (countable == nullptr) return 0; + + // pass the call to the interface +- return countable->count(); ++ // (explicit cast: on LP64 macOS 'long' matches no Php::Value constructor ++ // unambiguously because int64_t is 'long long' there) ++ return (int64_t)countable->count(); + } + + /** diff --git a/turbo-ext/phpstanturbo/CombinationsHelper.zep b/turbo-ext/phpstanturbo/CombinationsHelper.zep deleted file mode 100644 index cba8cea56de..00000000000 --- a/turbo-ext/phpstanturbo/CombinationsHelper.zep +++ /dev/null @@ -1,35 +0,0 @@ -namespace PHPStanTurbo; - -final class CombinationsHelper -{ - /** - * @param array arrays - * @return array - */ - public static function combinations(array arrays) -> array - { - var head, elem, combination, c, comb, subResult, results; - array remaining; - - if count(arrays) === 0 { - return [[]]; - } - - let remaining = arrays; - let head = array_shift(remaining); - let results = []; - - for elem in head { - let subResult = self::combinations(remaining); - for combination in subResult { - let comb = [elem]; - for c in combination { - let comb[] = c; - } - let results[] = comb; - } - } - - return results; - } -} diff --git a/turbo-ext/shadowed-classes.json b/turbo-ext/shadowed-classes.json new file mode 100644 index 00000000000..70eba3f5a93 --- /dev/null +++ b/turbo-ext/shadowed-classes.json @@ -0,0 +1,39 @@ +{ + "PHPStan\\Analyser\\ConditionalExpressionHolder": { + "php": "src/Analyser/ConditionalExpressionHolder.php", + "cpp": "turbo-ext/src/ConditionalExpressionHolder.cpp" + }, + "PHPStan\\Analyser\\ExpressionTypeHolder": { + "php": "src/Analyser/ExpressionTypeHolder.php", + "cpp": "turbo-ext/src/ExpressionTypeHolder.cpp" + }, + "PHPStan\\Analyser\\ScopeOps": { + "php": "src/Analyser/ScopeOps.php", + "cpp": "turbo-ext/src/ScopeOps.cpp" + }, + "PHPStan\\Internal\\CombinationsHelper": { + "php": "src/Internal/CombinationsHelper.php", + "cpp": "turbo-ext/src/CombinationsHelper.cpp" + }, + "PHPStan\\Node\\NodeScanner": { + "php": "src/Node/NodeScanner.php", + "cpp": "turbo-ext/src/NodeScanner.cpp" + }, + "PHPStan\\Parser\\ParserRunner": { + "php": "src/Parser/ParserRunner.php", + "cpp": "turbo-ext/src/parser/ParserRunner.cpp" + }, + "PHPStan\\TrinaryLogic": { + "php": "src/TrinaryLogic.php", + "cpp": "turbo-ext/src/TrinaryLogic.cpp" + }, + "PHPStan\\Type\\TypeCombinatorCache": { + "php": "src/Type/TypeCombinatorCache.php", + "cpp": "turbo-ext/src/TypeCombinatorCache.cpp" + }, + "PhpParser\\NodeTraverser": { + "php": "vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php", + "cpp": "turbo-ext/src/NodeTraverser.cpp", + "vendored": true + } +} diff --git a/turbo-ext/src/CombinationsHelper.cpp b/turbo-ext/src/CombinationsHelper.cpp new file mode 100644 index 00000000000..6aa20f40a28 --- /dev/null +++ b/turbo-ext/src/CombinationsHelper.cpp @@ -0,0 +1,165 @@ +/* + * PHPStanTurbo\CombinationsHelper — native implementation of + * PHPStan\Internal\CombinationsHelper::combinations() (Cartesian product). + */ + +#include "support.h" +#include "zv.h" + +static zend_class_entry *pt_ce_combinations = nullptr; + +namespace phpstanturbo { + +/* Mirrors PHPStan\Internal\CombinationsHelper. The PHP twin recurses through + * a generator; the native implementation materializes the full product + * iteratively, walking an odometer over the input arrays. */ +class CombinationsHelper +{ +public: + /* caps the result array's pre-allocation hint, not the product itself */ + static constexpr zend_ulong SIZE_HINT_LIMIT = 1048576; + + /* combinations() refuses products beyond this instead of letting the + * multiply overflow and silently truncate the result */ + static constexpr zend_ulong PRODUCT_LIMIT = 1ULL << 32; + + /* UNDEF = pending exception */ + static zv::Val combinations(zv::ArrRef arrays) + { + uint32_t n = arrays.size(); + if (n == 0) { + /* combinations([]) yields a single empty combination */ + zv::Arr result = zv::Arr::create(0); + result.push(zv::Arr::create(0)); + return result; + } + + /* borrow the inner array of each element (the input owns them) */ + zval **inner = (zval **) emalloc(n * sizeof(zval *)); + uint32_t i = 0; + for (auto entry : arrays) { + zv::Ref element = entry.value().deref(); + if (UNEXPECTED(!element.isArray())) { + efree(inner); + zend_type_error("PHPStanTurbo\\CombinationsHelper::combinations() expects an array of arrays"); + return zv::Val(); + } + inner[i++] = element.raw(); + } + + /* flatten each inner array into a raw element-slot vector */ + uint32_t *sizes = (uint32_t *) emalloc(n * sizeof(uint32_t)); + zval ***vecs = (zval ***) emalloc(n * sizeof(zval **)); + zend_ulong total = 1; + bool hasEmptyInner = false; + for (i = 0; i < n; i++) { + zv::ArrRef innerArr(inner[i]); + sizes[i] = innerArr.size(); + if (sizes[i] == 0) { + /* any empty input array empties the whole product */ + hasEmptyInner = true; + vecs[i] = NULL; + continue; + } + if (UNEXPECTED(total > PRODUCT_LIMIT / sizes[i])) { + /* The PHP twin is a lazy generator and never materializes the + * product, but every consumer iterates it fully, so a product + * this size is unreachable in practice. Failing loudly beats + * the silent truncation an overflowed multiply would cause. */ + for (uint32_t k = 0; k < i; k++) { + if (vecs[k] != NULL) { + efree(vecs[k]); + } + } + efree(vecs); + efree(sizes); + efree(inner); + pt_throw_should_not_happen(); + return zv::Val(); + } + total *= sizes[i]; + vecs[i] = (zval **) emalloc(sizes[i] * sizeof(zval *)); + uint32_t j = 0; + for (auto entry : innerArr) { + /* deref like the twin's by-value foreach: a reference slot + * must not propagate a shared reference into every combination */ + vecs[i][j++] = entry.value().deref().raw(); + } + } + + zv::Val result; + if (hasEmptyInner) { + result = zv::Arr::create(0); + } else { + result = product(vecs, sizes, n, total); + } + + for (i = 0; i < n; i++) { + if (vecs[i] != NULL) { + efree(vecs[i]); + } + } + efree(vecs); + efree(sizes); + efree(inner); + return result; + } + +private: + static zv::Val product(zval *const *const *vecs, const uint32_t *sizes, uint32_t n, zend_ulong total) + { + uint32_t *indices = (uint32_t *) ecalloc(n, sizeof(uint32_t)); + zv::Arr result = zv::Arr::create((uint32_t) (total > SIZE_HINT_LIMIT ? SIZE_HINT_LIMIT : total)); + + for (zend_ulong c = 0; c < total; c++) { + zv::Arr comb = zv::Arr::create(n); + for (uint32_t i = 0; i < n; i++) { + comb.push(zv::Ref(vecs[i][indices[i]])); + } + result.push(std::move(comb)); + + /* odometer: advance the rightmost index, carrying leftwards */ + for (int64_t j = (int64_t) n - 1; j >= 0; j--) { + if (++indices[j] < sizes[j]) { + break; + } + indices[j] = 0; + } + } + + efree(indices); + return result; + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::CombinationsHelper; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_register_combinations_helper() +{ + reg::Class cls("PHPStanTurbo\\CombinationsHelper"); + + cls.method("combinations", reg::PublicStatic, 1, { reg::arrayArg("arrays") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *arrays; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(arrays) + ZEND_PARSE_PARAMETERS_END(); + zval arraysZv; + ZVAL_ARR(&arraysZv, arrays); + zv::Val result = CombinationsHelper::combinations(zv::ArrRef(&arraysZv)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + /* not final: a PHP stub subclass may extend this class */ + pt_ce_combinations = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/ConditionalExpressionHolder.cpp b/turbo-ext/src/ConditionalExpressionHolder.cpp new file mode 100644 index 00000000000..ad4f91f492b --- /dev/null +++ b/turbo-ext/src/ConditionalExpressionHolder.cpp @@ -0,0 +1,120 @@ +/* + * PHPStanTurbo\ConditionalExpressionHolder — native implementation of + * PHPStan\Analyser\ConditionalExpressionHolder. + * + * Not final: a PHP stub subclass extends this class. The getKey() string is + * built by pt_ceh_key_build() in support.cpp, shared with ScopeOps. + */ + +#include "support.h" +#include "zv.h" + +namespace phpstanturbo { + +/* Mirrors PHPStan\Analyser\ConditionalExpressionHolder. State lives in the + * PHP object's properties. */ +class ConditionalExpressionHolder +{ +public: + explicit ConditionalExpressionHolder(zval *self) : self(self) {} + + /* false = pending exception (the twin throws on empty conditions) */ + bool construct(zv::ArrRef conditionExpressionTypeHolders, zv::Ref typeHolder) + { + if (UNEXPECTED(conditionExpressionTypeHolders.size() == 0)) { + pt_throw_should_not_happen(); + return false; + } + zv::ObjRef obj(self); + obj.propAtWrite(PT_CEH_PROP_CONDS, zv::Val::copyOf(conditionExpressionTypeHolders)); + obj.propAtWrite(PT_CEH_PROP_TYPEHOLDER, zv::Val::copyOf(typeHolder)); + return true; + } + + zv::Val getConditionExpressionTypeHolders() const + { + return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_CEH_PROP_CONDS)); + } + + zv::Val getTypeHolder() const + { + return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_CEH_PROP_TYPEHOLDER)); + } + + /* UNDEF = pending exception */ + zv::Val getKey() const + { + zv::ObjRef obj(self); + zv::Ref conds = obj.propAt(PT_CEH_PROP_CONDS); + zv::Ref typeHolder = obj.propAt(PT_CEH_PROP_TYPEHOLDER); + + for (auto entry : zv::ArrRef(conds.raw())) { + if (UNEXPECTED(!pt_check_holder(entry.value().deref().raw()))) { + return zv::Val(); + } + } + + zend_string *key = pt_ceh_key_build(conds.asArrayTable(), typeHolder.raw()); + if (UNEXPECTED(key == NULL)) { + return zv::Val(); + } + return zv::Val::adoptString(key); + } + +private: + zval *self; +}; + +} // namespace phpstanturbo + +using phpstanturbo::ConditionalExpressionHolder; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define ETH_CLASS "PHPStanTurbo\\ExpressionTypeHolder" + +void pt_register_conditional_expression_holder() +{ + reg::Class cls("PHPStanTurbo\\ConditionalExpressionHolder"); + /* not final: a PHP stub subclass extends this class; + * conditionExpressionTypeHolders/typeHolder must stay in this order */ + cls.privateNullProperty("conditionExpressionTypeHolders"); + cls.privateNullProperty("typeHolder"); + + cls.method("__construct", reg::Public, 2, { reg::arrayArg("conditionExpressionTypeHolders"), reg::obj("typeHolder", ETH_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *holders; + zval *typeHolder; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(holders) + Z_PARAM_OBJECT_OF_CLASS(typeHolder, pt_ce_expr_type_holder) + ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(!ConditionalExpressionHolder(ZEND_THIS).construct(zv::ArrRef(holders), zv::Ref(typeHolder)))) { + RETURN_THROWS(); + } + }); + + cls.method("getConditionExpressionTypeHolders", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ConditionalExpressionHolder(ZEND_THIS).getConditionExpressionTypeHolders().intoReturnValue(return_value); + }); + + cls.method("getTypeHolder", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ConditionalExpressionHolder(ZEND_THIS).getTypeHolder().intoReturnValue(return_value); + }); + + cls.method("getKey", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + zv::Val key = ConditionalExpressionHolder(ZEND_THIS).getKey(); + if (UNEXPECTED(key.isUndef())) { + RETURN_THROWS(); + } + key.intoReturnValue(return_value); + }); + + pt_ce_cond_expr_holder = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/ExpressionTypeHolder.cpp b/turbo-ext/src/ExpressionTypeHolder.cpp new file mode 100644 index 00000000000..14dc4823a5d --- /dev/null +++ b/turbo-ext/src/ExpressionTypeHolder.cpp @@ -0,0 +1,175 @@ +/* + * PHPStanTurbo\ExpressionTypeHolder — native implementation of + * PHPStan\Analyser\ExpressionTypeHolder. + * + * Not final: a PHP stub subclass extends this class, and instances are + * created from the configured expressionTypeHolderImpl class (see + * pt_holder_create() in support.cpp) so userland type hints keep working. + * + * The type/certainty logic stays in the shared pt_holder_* helpers + * (support.cpp), which ScopeOps also uses without crossing the method-call + * ABI; the handle class below is the method-level face over them, structured + * to mirror src/Analyser/ExpressionTypeHolder.php. + */ + +#include "support.h" +#include "zv.h" + +namespace phpstanturbo { + +/* Mirrors PHPStan\Analyser\ExpressionTypeHolder. State lives in the PHP + * object's expr/type/certainty properties. */ +class ExpressionTypeHolder +{ +public: + explicit ExpressionTypeHolder(zval *self) : self(self) {} + + void construct(zv::Ref expr, zv::Ref type, zv::Ref certainty) + { + zv::ObjRef obj(self); + obj.propAtWrite(PT_ETH_PROP_EXPR, zv::Val::copyOf(expr)); + obj.propAtWrite(PT_ETH_PROP_TYPE, zv::Val::copyOf(type)); + obj.propAtWrite(PT_ETH_PROP_CERTAINTY, zv::Val::copyOf(certainty)); + } + + static zv::Val createYes(zval *expr, zval *type) + { + zval holder; + pt_holder_create(&holder, expr, type, PT_TRI_YES); + return zv::Val::adopt(holder); + } + + static zv::Val createMaybe(zval *expr, zval *type) + { + zval holder; + pt_holder_create(&holder, expr, type, PT_TRI_MAYBE); + return zv::Val::adopt(holder); + } + + /* false = pending exception */ + bool equalTypes(zval *other, bool &out) const { return pt_holder_equal_types(self, other, &out); } + + /* false = pending exception */ + bool equals(zval *other, bool &out) const { return pt_holder_equals(self, other, &out); } + + /* and() — a C++ keyword, hence the underscore; UNDEF = pending exception */ + zv::Val and_(zval *other) const + { + zval result; + if (UNEXPECTED(!pt_holder_and(self, other, &result))) { + return zv::Val(); + } + return zv::Val::adopt(result); + } + + zv::Val getExpr() const { return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_ETH_PROP_EXPR)); } + zv::Val getType() const { return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_ETH_PROP_TYPE)); } + zv::Val getCertainty() const { return zv::Val::copyOf(zv::ObjRef(self).propAt(PT_ETH_PROP_CERTAINTY)); } + +private: + zval *self; +}; + +} // namespace phpstanturbo + +using phpstanturbo::ExpressionTypeHolder; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define ETH_CLASS "PHPStanTurbo\\ExpressionTypeHolder" +#define TRINARY_CLASS "PHPStanTurbo\\TrinaryLogic" + +void pt_register_expression_type_holder() +{ + reg::Class cls("PHPStanTurbo\\ExpressionTypeHolder"); + /* not final: a PHP stub subclass extends this class; expr/type/certainty + * must stay in this order (OBJ_PROP_NUM slots) */ + cls.privateNullProperty("expr"); + cls.privateNullProperty("type"); + cls.privateNullProperty("certainty"); + + cls.method("__construct", reg::Public, 3, { reg::any("expr"), reg::any("type"), reg::obj("certainty", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *type, *certainty; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(expr) + Z_PARAM_OBJECT(type) + Z_PARAM_OBJECT_OF_CLASS(certainty, pt_ce_trinary) + ZEND_PARSE_PARAMETERS_END(); + ExpressionTypeHolder(ZEND_THIS).construct(zv::Ref(expr), zv::Ref(type), zv::Ref(certainty)); + }); + + cls.method("createYes", reg::PublicStatic, 2, { reg::any("expr"), reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *type; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(expr) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + ExpressionTypeHolder::createYes(expr, type).intoReturnValue(return_value); + }); + + cls.method("createMaybe", reg::PublicStatic, 2, { reg::any("expr"), reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *type; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(expr) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + ExpressionTypeHolder::createMaybe(expr, type).intoReturnValue(return_value); + }); + + cls.method("equalTypes", reg::Public, 1, { reg::obj("other", ETH_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + bool out; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_expr_type_holder) + ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(!ExpressionTypeHolder(ZEND_THIS).equalTypes(other, out))) { + RETURN_THROWS(); + } + RETURN_BOOL(out); + }); + + cls.method("equals", reg::Public, 1, { reg::obj("other", ETH_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + bool out; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_expr_type_holder) + ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(!ExpressionTypeHolder(ZEND_THIS).equals(other, out))) { + RETURN_THROWS(); + } + RETURN_BOOL(out); + }); + + cls.method("and", reg::Public, 1, { reg::obj("other", ETH_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_expr_type_holder) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ExpressionTypeHolder(ZEND_THIS).and_(other); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("getExpr", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ExpressionTypeHolder(ZEND_THIS).getExpr().intoReturnValue(return_value); + }); + + cls.method("getType", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ExpressionTypeHolder(ZEND_THIS).getType().intoReturnValue(return_value); + }); + + cls.method("getCertainty", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + ExpressionTypeHolder(ZEND_THIS).getCertainty().intoReturnValue(return_value); + }); + + pt_ce_expr_type_holder = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/NodeScanner.cpp b/turbo-ext/src/NodeScanner.cpp new file mode 100644 index 00000000000..f0ee16aa07f --- /dev/null +++ b/turbo-ext/src/NodeScanner.cpp @@ -0,0 +1,73 @@ +/* + * PHPStanTurbo\NodeScanner — native node scanning helpers. + * + * nodeIsOrContainsYield(): recursively checks whether the given node is or + * contains a Yield_/YieldFrom expression, using the shared findFirst walker + * from the support layer. Ported from the proven ScopeOps C implementation. + */ + +#include "support.h" +#include "zv.h" + +static zend_class_entry *pt_ce_node_scanner; + +namespace phpstanturbo { + +/* Mirrors PHPStan\Node\NodeScanner. */ +class NodeScanner +{ +public: + /* failed = pending exception */ + static bool nodeIsOrContainsYield(zv::ObjRef node, bool &failed) + { + pt_find_ctx ctx = {}; + zend_object *found = pt_find_first_recursive(node.raw(), isYieldNode, &ctx); + failed = ctx.failed; + return found != NULL; + } + +private: + /* matcher for the shared findFirst walker */ + static bool isYieldNode(zend_object *node, void *ctx) + { + zend_class_entry *yieldCe = pt_class(PT_CLASS_YIELD); + zend_class_entry *yieldFromCe = pt_class(PT_CLASS_YIELD_FROM); + + if (UNEXPECTED(yieldCe == NULL || yieldFromCe == NULL)) { + ((pt_find_ctx *) ctx)->failed = true; + return false; + } + return instanceof_function(node->ce, yieldCe) || instanceof_function(node->ce, yieldFromCe); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::NodeScanner; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_register_node_scanner() +{ + reg::Class cls("PHPStanTurbo\\NodeScanner"); + + cls.method("nodeIsOrContainsYield", reg::PublicStatic, 1, { reg::objectArg("node") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(node) + ZEND_PARSE_PARAMETERS_END(); + bool failed = false; + bool result = NodeScanner::nodeIsOrContainsYield(zv::ObjRef(node), failed); + if (UNEXPECTED(failed)) { + RETURN_THROWS(); + } + RETURN_BOOL(result); + }); + + /* not final */ + pt_ce_node_scanner = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/NodeTraverser.cpp b/turbo-ext/src/NodeTraverser.cpp new file mode 100644 index 00000000000..42472da3346 --- /dev/null +++ b/turbo-ext/src/NodeTraverser.cpp @@ -0,0 +1,912 @@ +/* + * PHPStanTurbo\NodeTraverser — native reimplementation of + * PhpParser\NodeTraverser (a PHP stub subclass PhpParser\NodeTraverser + * extends this class when the extension is loaded). + * + * The logic lives in the NodeTraverser handle class below, structured to + * mirror vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.php method for + * method — traverse()/traverseNode()/traverseArray() and the visitor + * return-value protocol; the PHP_METHOD functions at the bottom are only the + * engine ABI glue (parameter parsing + delegation). Additionally, visitors + * that inherit enterNode()/leaveNode()/beforeTraverse()/afterTraverse() + * unchanged from NodeVisitorAbstract are not called for that hook (the + * inherited hook returns null, so skipping it is unobservable). + */ + +#include "support.h" +#include "zv.h" + +static zend_class_entry *pt_ce_node_traverser; + +#define PT_NT_PROP_VISITORS 0 +#define PT_NT_PROP_STOP 1 + +/* {{{ pt_* traversal substrate */ + +/* Per-visitor call plan built once per traverse(): the visitor object and + * its cached hook functions. NULL before_fn/after_fn and false + * call_enter/call_leave mean "inherited no-op from NodeVisitorAbstract, + * skip the call". */ +typedef struct _pt_visitor_plan { + zend_object *visitor; + zend_class_entry *ce; + zend_function *enter_fn; + zend_function *leave_fn; + zend_function *before_fn; + zend_function *after_fn; + bool call_enter; + bool call_leave; +} pt_visitor_plan; + +/* The splices recorded by one traverseArray() pass ($doNodes in the twin). */ +typedef struct _pt_do_node { + zend_ulong pos; + zval replacement; /* IS_ARRAY (owned) or IS_FALSE for remove */ +} pt_do_node; + +typedef struct _pt_do_nodes { + pt_do_node *items; + uint32_t count; + uint32_t capacity; +} pt_do_nodes; + +static void pt_do_nodes_push(pt_do_nodes *dn, zend_ulong pos, zval *replacement) +{ + if (dn->count == dn->capacity) { + dn->capacity = dn->capacity == 0 ? 4 : dn->capacity * 2; + dn->items = (pt_do_node *) erealloc(dn->items, dn->capacity * sizeof(pt_do_node)); + } + dn->items[dn->count].pos = pos; + if (replacement != NULL) { + ZVAL_COPY(&dn->items[dn->count].replacement, replacement); + } else { + ZVAL_FALSE(&dn->items[dn->count].replacement); + } + dn->count++; +} + +static void pt_do_nodes_free(pt_do_nodes *dn) +{ + uint32_t i; + for (i = 0; i < dn->count; i++) { + zval_ptr_dtor(&dn->items[i].replacement); + } + if (dn->items != NULL) { + efree(dn->items); + } +} + +static void pt_trav_throw_logic(const char *format, const char *arg) +{ + zend_string *name = zend_string_init("LogicException", sizeof("LogicException") - 1, 0); + zend_class_entry *ce = zend_lookup_class(name); + zend_string_release(name); + if (ce == NULL) { + zend_throw_error(NULL, format, arg); + return; + } + zend_throw_exception_ex(ce, 0, format, arg); +} + +/* Node subnode info incl. names, resolved lazily per class. */ +typedef struct _pt_trav_class_info { + uint32_t *offsets; + zend_string **names; + uint32_t count; + bool resolved; +} pt_trav_class_info; + +static HashTable pt_trav_class_cache; +static bool pt_trav_class_cache_inited; + +static void pt_trav_class_info_free(zval *zv) +{ + pt_trav_class_info *info = (pt_trav_class_info *) Z_PTR_P(zv); + uint32_t i; + for (i = 0; i < info->count; i++) { + zend_string_release(info->names[i]); + } + if (info->offsets != NULL) { + efree(info->offsets); + } + if (info->names != NULL) { + efree(info->names); + } + efree(info); +} + +void pt_node_traverser_rinit() +{ + pt_trav_class_cache_inited = false; +} + +void pt_node_traverser_rshutdown() +{ + if (pt_trav_class_cache_inited) { + zend_hash_destroy(&pt_trav_class_cache); + pt_trav_class_cache_inited = false; + } +} + +static pt_trav_class_info *pt_trav_class_info_for(zend_object *obj) +{ + zend_class_entry *ce = obj->ce; + pt_trav_class_info *info; + zend_function *fn; + zval names; + + if (!pt_trav_class_cache_inited) { + zend_hash_init(&pt_trav_class_cache, 64, NULL, pt_trav_class_info_free, 0); + pt_trav_class_cache_inited = true; + } + + info = (pt_trav_class_info *) zend_hash_find_ptr(&pt_trav_class_cache, ce->name); + if (EXPECTED(info != NULL)) { + return info; + } + + info = (pt_trav_class_info *) ecalloc(1, sizeof(pt_trav_class_info)); + + fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "getsubnodenames", sizeof("getsubnodenames") - 1); + if (fn != NULL && (fn->common.fn_flags & ZEND_ACC_ABSTRACT) == 0) { + zend_call_known_function(fn, obj, ce, &names, 0, NULL, NULL); + if (!EG(exception) && Z_TYPE(names) == IS_ARRAY) { + uint32_t capacity = zend_hash_num_elements(Z_ARRVAL(names)); + zval *name_zv; + info->offsets = (uint32_t *) emalloc(sizeof(uint32_t) * (capacity > 0 ? capacity : 1)); + info->names = (zend_string **) emalloc(sizeof(zend_string *) * (capacity > 0 ? capacity : 1)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(names), name_zv) { + zend_property_info *prop; + if (Z_TYPE_P(name_zv) != IS_STRING) { + continue; + } + prop = (zend_property_info *) zend_hash_find_ptr(&ce->properties_info, Z_STR_P(name_zv)); + if (prop == NULL || (prop->flags & ZEND_ACC_STATIC) != 0) { + continue; + } + info->offsets[info->count] = (uint32_t) prop->offset; + info->names[info->count] = zend_string_copy(Z_STR_P(name_zv)); + info->count++; + } ZEND_HASH_FOREACH_END(); + } + zval_ptr_dtor(&names); + } + + zend_hash_add_ptr(&pt_trav_class_cache, ce->name, info); + return info; +} + +/* }}} */ + +namespace phpstanturbo { + +/* + * Mirrors PhpParser\NodeTraverser. The visitor list and the stopTraversal + * flag live in the PHP object's properties; this handle carries the state of + * one traverse() call (the visitor plan and the stop/failed flags). + */ +class NodeTraverser +{ +public: + /* NodeVisitor protocol constants (frozen public API of php-parser) */ + static constexpr zend_long DONT_TRAVERSE_CHILDREN = 1; + static constexpr zend_long STOP_TRAVERSAL = 2; + static constexpr zend_long REMOVE_NODE = 3; + static constexpr zend_long DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4; + static constexpr zend_long REPLACE_WITH_NULL = 5; + + explicit NodeTraverser(zend_object *self) : self(self) {} + + NodeTraverser(const NodeTraverser &) = delete; + NodeTraverser &operator=(const NodeTraverser &) = delete; + + ~NodeTraverser() + { + if (plan != NULL) { + /* the plan owns its visitor references: removeVisitor() (or any + * userland write to $this->visitors) during traversal must not be + * able to free a visitor the plan still dispatches to */ + for (uint32_t i = 0; i < nplanned; i++) { + OBJ_RELEASE(plan[i].visitor); + } + efree(plan); + } + } + + /* __construct(NodeVisitor ...$visitors); false means an argument error + * was thrown */ + bool construct(zval *visitors, uint32_t count) + { + zv::Arr list = zv::Arr::create(count); + for (uint32_t i = 0; i < count; i++) { + zv::Ref visitor = zv::Ref(&visitors[i]).deref(); + if (UNEXPECTED(!visitor.isObject())) { + zend_argument_type_error(i + 1, "must be of type PhpParser\\NodeVisitor"); + return false; + } + list.push(visitor); + } + zv::ObjRef(self).propAtWrite(PT_NT_PROP_VISITORS, std::move(list)); + return true; + } + + /* $this->visitors[] = $visitor */ + void addVisitor(zv::Ref visitor) + { + zv::Ref prop = visitorsProp(); + if (!prop.isArray()) { + prop.assign(zv::Arr::create(0)); + } + zv::ArrRef(prop.raw()).push(visitor); + } + + void removeVisitor(zv::Ref visitor) + { + zv::Ref prop = visitorsProp(); + if (!prop.isArray()) { + return; + } + + /* array_search() with loose comparison, like the PHP implementation */ + bool found = false; + zend_ulong foundPos = 0; + zend_ulong pos = 0; + for (auto entry : zv::ArrRef(prop.raw())) { + zv::Ref candidate = entry.value().deref(); + if (candidate.isObject() && zend_compare(candidate.raw(), visitor.raw()) == 0) { + found = true; + foundPos = pos; + break; + } + pos++; + } + if (!found) { + return; + } + + /* array_splice($visitors, $index, 1, []) — reindexes */ + zv::ArrRef old(prop.raw()); + zv::Arr rebuilt = zv::Arr::create(old.size()); + pos = 0; + for (auto entry : old) { + if (pos++ != foundPos) { + rebuilt.push(entry.value()); + } + } + prop.assign(std::move(rebuilt)); + } + + /* traverse(); UNDEF result means a pending exception */ + zv::Val traverse(HashTable *nodesTable) + { + zv::ObjRef selfObj(self); + + /* $this->stopTraversal = false */ + selfObj.propAtWrite(PT_NT_PROP_STOP, zv::Val::boolean(false)); + + if (UNEXPECTED(!buildVisitorPlan())) { + return zv::Val(); + } + + /* work on our own copy of the nodes array */ + zv::Arr nodes = zv::Arr::adoptTable(zend_array_dup(nodesTable)); + + /* beforeTraverse */ + for (uint32_t vi = 0; vi < nvisitors; vi++) { + const pt_visitor_plan *p = &plan[vi]; + if (p->before_fn == NULL) { + continue; + } + zv::Val ret = callVisitorHook(p, p->before_fn, nodes.ref()); + if (UNEXPECTED(ret.isUndef())) { + return zv::Val(); + } + if (ret.ref().isArray()) { + nodes = zv::Arr::adoptVal(std::move(ret)); + nodes.separate(); + } + } + + nodes.separate(); + zv::Arr replacement = traverseArray(nodes.arrRef()); + if (UNEXPECTED(failed)) { + return zv::Val(); + } + if (!replacement.isUndef()) { + nodes = std::move(replacement); + } + + /* afterTraverse, in reverse */ + for (int64_t vi = (int64_t) nvisitors - 1; vi >= 0; vi--) { + const pt_visitor_plan *p = &plan[vi]; + if (p->after_fn == NULL) { + continue; + } + zv::Val ret = callVisitorHook(p, p->after_fn, nodes.ref()); + if (UNEXPECTED(ret.isUndef())) { + return zv::Val(); + } + if (ret.ref().isArray()) { + nodes = zv::Arr::adoptVal(std::move(ret)); + } + } + + /* persist stopTraversal like the PHP implementation */ + selfObj.propAtWrite(PT_NT_PROP_STOP, zv::Val::boolean(stop)); + + return nodes; + } + +private: + /* Mirrors traverseNode(): recursively traverse the subnodes of a node, + * read via the per-class property offset cache. */ + void traverseNode(zend_object *node) + { + pt_trav_class_info *info = pt_trav_class_info_for(node); + zend_class_entry *nodeIface = pt_class(PT_CLASS_NODE); + + if (UNEXPECTED(EG(exception))) { + failed = true; + return; + } + if (UNEXPECTED(info == NULL || nodeIface == NULL)) { + failed = true; + return; + } + + for (uint32_t i = 0; i < info->count; i++) { + zv::Ref value = zv::Ref(OBJ_PROP(node, info->offsets[i])).deref(); + + if (value.isArray()) { + /* separate so we can mutate in place */ + SEPARATE_ARRAY(value.raw()); + zv::Arr replacement = traverseArray(zv::ArrRef(value.raw())); + if (UNEXPECTED(failed)) { + return; + } + if (!replacement.isUndef()) { + value.assign(std::move(replacement)); + } + if (stop) { + return; + } + continue; + } + + if (!value.instanceOf(nodeIface)) { + continue; + } + /* Own a reference for the whole block: a visitor writing to the + * parent's property from a hook can otherwise drop the node's + * last reference while later hooks still run on it. The PHP twin + * survives that by construction ($subNode owns a reference). */ + zv::Val subNodeOwned = zv::Val::copyOf(zv::Ref(value.raw())); + zend_object *subNode = value.asObject(); + + bool traverseChildren = true; + int64_t visitorIndex = -1; + bool skipToNext = false; + + /* enterNode */ + for (uint32_t vi = 0; vi < nvisitors; vi++) { + const pt_visitor_plan *p = &plan[vi]; + visitorIndex = vi; + if (!p->call_enter) { + continue; + } + zv::Val ret = callVisitorHook(p, p->enter_fn, subNode); + if (UNEXPECTED(ret.isUndef())) { + failed = true; + return; + } + zv::Ref retRef = ret.ref(); + if (retRef.isNull()) { + continue; + } + if (retRef.instanceOf(nodeIface)) { + if (UNEXPECTED(!ensureReplacementReasonable(subNode, retRef.asObject()))) { + failed = true; + return; + } + /* $node->$name = $subNode = $return */ + if (UNEXPECTED(!writeSubnode(node, info->names[i], retRef))) { + return; + } + subNodeOwned = zv::Val::copyOf(retRef); + subNode = retRef.asObject(); + continue; + } + if (retRef.isLong()) { + zend_long code = retRef.asLong(); + if (code == DONT_TRAVERSE_CHILDREN) { + traverseChildren = false; + continue; + } + if (code == DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + traverseChildren = false; + break; + } + if (code == STOP_TRAVERSAL) { + stop = true; + return; + } + if (code == REPLACE_WITH_NULL) { + if (UNEXPECTED(!writeSubnodeNull(node, info->names[i]))) { + return; + } + skipToNext = true; + break; + } + } + pt_trav_throw_logic("enterNode() returned invalid value of type %s", zend_zval_value_name(retRef.raw())); + failed = true; + return; + } + + if (skipToNext) { + continue; + } + + if (traverseChildren) { + traverseNode(subNode); + if (UNEXPECTED(failed) || stop) { + return; + } + } + + /* leaveNode, in reverse from the last visitor whose enterNode ran */ + for (int64_t vi = visitorIndex; vi >= 0; vi--) { + const pt_visitor_plan *p = &plan[vi]; + if (!p->call_leave) { + continue; + } + zv::Val ret = callVisitorHook(p, p->leave_fn, subNode); + if (UNEXPECTED(ret.isUndef())) { + failed = true; + return; + } + zv::Ref retRef = ret.ref(); + if (retRef.isNull()) { + continue; + } + if (retRef.instanceOf(nodeIface)) { + if (UNEXPECTED(!ensureReplacementReasonable(subNode, retRef.asObject()))) { + failed = true; + return; + } + if (UNEXPECTED(!writeSubnode(node, info->names[i], retRef))) { + return; + } + subNodeOwned = zv::Val::copyOf(retRef); + subNode = retRef.asObject(); + continue; + } + if (retRef.isLong()) { + zend_long code = retRef.asLong(); + if (code == STOP_TRAVERSAL) { + stop = true; + return; + } + if (code == REPLACE_WITH_NULL) { + if (UNEXPECTED(!writeSubnodeNull(node, info->names[i]))) { + return; + } + break; + } + } + if (retRef.isArray()) { + pt_trav_throw_logic("leaveNode() may only return an array if the parent structure is an array%s", ""); + failed = true; + return; + } + pt_trav_throw_logic("leaveNode() returned invalid value of type %s", zend_zval_value_name(retRef.raw())); + failed = true; + return; + } + } + } + + /* + * Mirrors traverseArray(): traverses the table behind `nodes` in place + * (the caller passes a separated, exclusively-owned array) and returns + * the rebuilt array when splices (REMOVE_NODE / replacement arrays) were + * recorded, an UNDEF Arr otherwise. + */ + zv::Arr traverseArray(zv::ArrRef nodes) + { + pt_do_nodes doNodes = {}; + zend_class_entry *nodeIface = pt_class(PT_CLASS_NODE); + if (UNEXPECTED(nodeIface == NULL)) { + failed = true; + return zv::Arr(); + } + + zend_ulong pos = 0; + for (auto nodesEntry : nodes) { + zend_ulong i = pos++; + zv::Ref slot = nodesEntry.value(); + zv::Ref value = slot.deref(); + + if (!value.instanceOf(nodeIface)) { + if (UNEXPECTED(value.isArray())) { + pt_trav_throw_logic("Invalid node structure: Contains nested arrays%s", ""); + failed = true; + break; + } + continue; + } + zend_object *node = value.asObject(); + + bool traverseChildren = true; + int64_t visitorIndex = -1; + bool skipToNext = false; + + /* enterNode */ + for (uint32_t vi = 0; vi < nvisitors; vi++) { + const pt_visitor_plan *p = &plan[vi]; + visitorIndex = vi; + if (!p->call_enter) { + continue; + } + zv::Val ret = callVisitorHook(p, p->enter_fn, node); + if (UNEXPECTED(ret.isUndef())) { + failed = true; + break; + } + zv::Ref retRef = ret.ref(); + if (retRef.isNull()) { + continue; + } + if (retRef.instanceOf(nodeIface)) { + if (UNEXPECTED(!ensureReplacementReasonable(node, retRef.asObject()))) { + failed = true; + break; + } + /* $nodes[$i] = $node = $return */ + node = retRef.asObject(); + slot.assign(std::move(ret)); + continue; + } + if (retRef.isArray()) { + pt_do_nodes_push(&doNodes, i, retRef.raw()); + skipToNext = true; + break; + } + if (retRef.isLong()) { + zend_long code = retRef.asLong(); + if (code == REMOVE_NODE) { + pt_do_nodes_push(&doNodes, i, NULL); + skipToNext = true; + break; + } + if (code == DONT_TRAVERSE_CHILDREN) { + traverseChildren = false; + continue; + } + if (code == DONT_TRAVERSE_CURRENT_AND_CHILDREN) { + traverseChildren = false; + break; + } + if (code == STOP_TRAVERSAL) { + stop = true; + break; + } + if (code == REPLACE_WITH_NULL) { + pt_trav_throw_logic("REPLACE_WITH_NULL can not be used if the parent structure is an array%s", ""); + failed = true; + break; + } + } + pt_trav_throw_logic("enterNode() returned invalid value of type %s", zend_zval_value_name(retRef.raw())); + failed = true; + break; + } + + if (UNEXPECTED(failed) || stop) { + break; + } + if (skipToNext) { + continue; + } + + if (traverseChildren) { + traverseNode(node); + if (UNEXPECTED(failed) || stop) { + break; + } + } + + /* leaveNode, in reverse from the last visitor whose enterNode ran */ + for (int64_t vi = visitorIndex; vi >= 0; vi--) { + const pt_visitor_plan *p = &plan[vi]; + if (!p->call_leave) { + continue; + } + zv::Val ret = callVisitorHook(p, p->leave_fn, node); + if (UNEXPECTED(ret.isUndef())) { + failed = true; + break; + } + zv::Ref retRef = ret.ref(); + if (retRef.isNull()) { + continue; + } + if (retRef.instanceOf(nodeIface)) { + if (UNEXPECTED(!ensureReplacementReasonable(node, retRef.asObject()))) { + failed = true; + break; + } + node = retRef.asObject(); + slot.assign(std::move(ret)); + continue; + } + if (retRef.isArray()) { + pt_do_nodes_push(&doNodes, i, retRef.raw()); + break; + } + if (retRef.isLong()) { + zend_long code = retRef.asLong(); + if (code == REMOVE_NODE) { + pt_do_nodes_push(&doNodes, i, NULL); + break; + } + if (code == STOP_TRAVERSAL) { + stop = true; + break; + } + if (code == REPLACE_WITH_NULL) { + pt_trav_throw_logic("REPLACE_WITH_NULL can not be used if the parent structure is an array%s", ""); + failed = true; + break; + } + } + pt_trav_throw_logic("leaveNode() returned invalid value of type %s", zend_zval_value_name(retRef.raw())); + failed = true; + break; + } + + if (UNEXPECTED(failed) || stop) { + break; + } + } + + if (UNEXPECTED(failed)) { + pt_do_nodes_free(&doNodes); + return zv::Arr(); + } + + zv::Arr rebuilt; + if (doNodes.count > 0) { + /* apply the recorded splices in one rebuild pass */ + rebuilt = zv::Arr::create(nodes.size()); + uint32_t cursor = 0; + zend_ulong rebuildPos = 0; + for (auto nodesEntry : nodes) { + if (cursor < doNodes.count && doNodes.items[cursor].pos == rebuildPos) { + zv::Ref replacement(&doNodes.items[cursor].replacement); + if (replacement.isArray()) { + for (auto replacementEntry : zv::ArrRef(replacement.raw())) { + rebuilt.push(replacementEntry.value()); + } + } + /* IS_FALSE => removed, append nothing */ + cursor++; + } else { + rebuilt.push(nodesEntry.value()); + } + rebuildPos++; + } + } + + pt_do_nodes_free(&doNodes); + return rebuilt; + } + + /* $old instanceof Stmt && $new instanceof Expr (and vice versa) + * => LogicException; false means it threw */ + static bool ensureReplacementReasonable(zend_object *oldNode, zend_object *newNode) + { + zend_class_entry *stmtCe = pt_class(PT_CLASS_STMT); + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(stmtCe == NULL || exprCe == NULL)) { + return false; + } + + zv::ObjRef oldRef(oldNode); + zv::ObjRef newRef(newNode); + if (oldRef.instanceOf(stmtCe) && newRef.instanceOf(exprCe)) { + pt_trav_throw_logic("Trying to replace statement with expression. Are you missing a Stmt_Expression wrapper?%s", ""); + return false; + } + if (oldRef.instanceOf(exprCe) && newRef.instanceOf(stmtCe)) { + pt_trav_throw_logic("Trying to replace expression with statement%s", ""); + return false; + } + return true; + } + + /* Builds the per-visitor hook plan from $this->visitors; false on error. */ + bool buildVisitorPlan() + { + zv::Ref visitors = visitorsProp(); + if (UNEXPECTED(!visitors.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: NodeTraverser visitors is not an array"); + return false; + } + + zend_function *baseEnter = NULL; + zend_function *baseLeave = NULL; + zend_function *baseBefore = NULL; + zend_function *baseAfter = NULL; + zend_class_entry *abstractCe = pt_class(PT_CLASS_NODE_VISITOR_ABSTRACT); + if (abstractCe != NULL) { + baseEnter = findHook(abstractCe, "enternode", sizeof("enternode") - 1); + baseLeave = findHook(abstractCe, "leavenode", sizeof("leavenode") - 1); + baseBefore = findHook(abstractCe, "beforetraverse", sizeof("beforetraverse") - 1); + baseAfter = findHook(abstractCe, "aftertraverse", sizeof("aftertraverse") - 1); + } else if (EG(exception)) { + zend_clear_exception(); + } + + zv::ArrRef visitorList(visitors.raw()); + nvisitors = visitorList.size(); + plan = nvisitors > 0 ? (pt_visitor_plan *) emalloc(nvisitors * sizeof(pt_visitor_plan)) : NULL; + + uint32_t i = 0; + for (auto entry : visitorList) { + zv::Ref visitor = entry.value().deref(); + if (UNEXPECTED(!visitor.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: NodeTraverser visitor is not an object"); + return false; + } + pt_visitor_plan *p = &plan[i]; + p->visitor = visitor.asObject(); + GC_ADDREF(p->visitor); + nplanned = i + 1; + p->ce = p->visitor->ce; + p->enter_fn = findHook(p->ce, "enternode", sizeof("enternode") - 1); + p->leave_fn = findHook(p->ce, "leavenode", sizeof("leavenode") - 1); + if (UNEXPECTED(p->enter_fn == NULL || p->leave_fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: visitor %s lacks enterNode/leaveNode", ZSTR_VAL(p->ce->name)); + return false; + } + p->call_enter = p->enter_fn != baseEnter; + p->call_leave = p->leave_fn != baseLeave; + p->before_fn = findHook(p->ce, "beforetraverse", sizeof("beforetraverse") - 1); + p->after_fn = findHook(p->ce, "aftertraverse", sizeof("aftertraverse") - 1); + if (p->before_fn == baseBefore) { + p->before_fn = NULL; + } + if (p->after_fn == baseAfter) { + p->after_fn = NULL; + } + i++; + } + + return true; + } + + /* hook lookup by lowercased name; NULL when the class lacks the method */ + static zend_function *findHook(zend_class_entry *ce, const char *lcname, size_t len) + { + return (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + } + + /* calls one visitor hook through its cached zend_function; + * UNDEF result means a pending exception */ + zv::Val callVisitorHook(const pt_visitor_plan *p, zend_function *hook, zv::Ref arg) const + { + zval retval; + zend_call_known_function(hook, p->visitor, p->ce, &retval, 1, arg.raw(), NULL); + if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + return zv::Val::adopt(retval); + } + + zv::Val callVisitorHook(const pt_visitor_plan *p, zend_function *hook, zend_object *node) const + { + zval nodeZv; + ZVAL_OBJ(&nodeZv, node); + return callVisitorHook(p, hook, zv::Ref(&nodeZv)); + } + + /* $node->$name = $value via the engine write path; false when it threw */ + bool writeSubnode(zend_object *node, zend_string *name, zv::Ref value) + { + zv::ObjRef(node).propWrite(name, value); + if (UNEXPECTED(EG(exception))) { + failed = true; + return false; + } + return true; + } + + bool writeSubnodeNull(zend_object *node, zend_string *name) + { + zv::Val nullValue = zv::Val::null(); + return writeSubnode(node, name, nullValue.ref()); + } + + zv::Ref visitorsProp() const + { + return zv::ObjRef(self).propAt(PT_NT_PROP_VISITORS).deref(); + } + + zend_object *self; + pt_visitor_plan *plan = NULL; + uint32_t nvisitors = 0; + /* how many plan entries own a visitor reference (build can fail mid-way) */ + uint32_t nplanned = 0; + bool stop = false; + bool failed = false; +}; + +} // namespace phpstanturbo + +using phpstanturbo::NodeTraverser; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define NODE_VISITOR_CLASS "PhpParser\\NodeVisitor" + +void pt_register_node_traverser() +{ + reg::Class cls("PHPStanTurbo\\NodeTraverser"); + /* not final: the stub subclass PhpParser\NodeTraverser extends this class; + * "visitors" must stay slot 0 and "stopTraversal" slot 1 (PT_NT_PROP_*) */ + cls.protectedArrayProperty("visitors"); + cls.protectedBoolProperty("stopTraversal", false); + + cls.classConstantLong("DONT_TRAVERSE_CHILDREN", NodeTraverser::DONT_TRAVERSE_CHILDREN); + cls.classConstantLong("STOP_TRAVERSAL", NodeTraverser::STOP_TRAVERSAL); + cls.classConstantLong("REMOVE_NODE", NodeTraverser::REMOVE_NODE); + cls.classConstantLong("DONT_TRAVERSE_CURRENT_AND_CHILDREN", NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN); + + cls.method("__construct", reg::Public, 0, { reg::variadicObj("visitors", NODE_VISITOR_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *visitors = NULL; + uint32_t count = 0; + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('+', visitors, count) + ZEND_PARSE_PARAMETERS_END(); + NodeTraverser self(Z_OBJ_P(ZEND_THIS)); + if (UNEXPECTED(!self.construct(visitors, count))) { + RETURN_THROWS(); + } + }); + + cls.method("addVisitor", reg::Public, 1, { reg::obj("visitor", NODE_VISITOR_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *visitor; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(visitor) + ZEND_PARSE_PARAMETERS_END(); + NodeTraverser(Z_OBJ_P(ZEND_THIS)).addVisitor(zv::Ref(visitor)); + }); + + cls.method("removeVisitor", reg::Public, 1, { reg::obj("visitor", NODE_VISITOR_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *visitor; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(visitor) + ZEND_PARSE_PARAMETERS_END(); + NodeTraverser(Z_OBJ_P(ZEND_THIS)).removeVisitor(zv::Ref(visitor)); + }); + + cls.method("traverse", reg::Public, 1, { reg::arrayArg("nodes") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *nodes; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(nodes) + ZEND_PARSE_PARAMETERS_END(); + NodeTraverser self(Z_OBJ_P(ZEND_THIS)); + zv::Val result = self.traverse(nodes); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + pt_ce_node_traverser = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp new file mode 100644 index 00000000000..c0fd2546de9 --- /dev/null +++ b/turbo-ext/src/ScopeOps.cpp @@ -0,0 +1,2198 @@ +/* + * PHPStanTurbo\ScopeOps — native implementations of MutatingScope's hottest + * scope-table loops: merging, conditional-expression bookkeeping, expression + * invalidation (including a native replacement for the NodeFinder-based AST + * walk) and clone-based scope creation. + * + * The ScopeOps handle class below mirrors src/Analyser/ScopeOps.php method + * for method, in the same order; where a body departs from the twin's shape + * for performance the comment on the method says so. The PHP_METHOD functions + * at the bottom are only the engine ABI glue (parameter parsing + delegation). + * Not final: a PHP stub subclass extends this class, so PHP code calls the + * methods through the shadowed class. + */ + +#include "support.h" +#include "zv.h" + +#include + +static zend_class_entry *pt_ce_scope_ops; + +/* {{{ scopeWith's cached property layout (owned by the request lifecycle) */ + +typedef struct _pt_scope_offsets { + int32_t expression_types; + int32_t native_expression_types; + int32_t conditional_expressions; + int32_t currently_assigned; + int32_t currently_allowed_undefined; + int32_t in_function_calls_stack; + int32_t in_first_level_statement; + int32_t after_extract_call; + /* memo props to reset (missing => -1) */ + int32_t resolved_types; + int32_t truthy_scopes; + int32_t falsey_scopes; + int32_t fiber_scope; + int32_t scope_out_of_first_level; + int32_t scope_with_promoted_native; + int32_t mutating_scope; /* FiberScope */ + int32_t truthy_value_exprs; /* FiberScope */ + int32_t falsey_value_exprs; /* FiberScope */ +} pt_scope_offsets; + +static HashTable pt_scope_offsets_cache; +static bool pt_scope_offsets_cache_inited = false; + +static void pt_scope_offsets_free(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +static pt_scope_offsets *pt_scope_offsets_for(zend_class_entry *ce) +{ + pt_scope_offsets *off; + + if (!pt_scope_offsets_cache_inited) { + zend_hash_init(&pt_scope_offsets_cache, 4, NULL, pt_scope_offsets_free, 0); + pt_scope_offsets_cache_inited = true; + } + off = (pt_scope_offsets *) zend_hash_find_ptr(&pt_scope_offsets_cache, ce->name); + if (EXPECTED(off != NULL)) { + return off; + } + + off = (pt_scope_offsets *) emalloc(sizeof(pt_scope_offsets)); + off->expression_types = pt_instance_prop_offset(ce, "expressionTypes", sizeof("expressionTypes") - 1); + off->native_expression_types = pt_instance_prop_offset(ce, "nativeExpressionTypes", sizeof("nativeExpressionTypes") - 1); + off->conditional_expressions = pt_instance_prop_offset(ce, "conditionalExpressions", sizeof("conditionalExpressions") - 1); + off->currently_assigned = pt_instance_prop_offset(ce, "currentlyAssignedExpressions", sizeof("currentlyAssignedExpressions") - 1); + off->currently_allowed_undefined = pt_instance_prop_offset(ce, "currentlyAllowedUndefinedExpressions", sizeof("currentlyAllowedUndefinedExpressions") - 1); + off->in_function_calls_stack = pt_instance_prop_offset(ce, "inFunctionCallsStack", sizeof("inFunctionCallsStack") - 1); + off->in_first_level_statement = pt_instance_prop_offset(ce, "inFirstLevelStatement", sizeof("inFirstLevelStatement") - 1); + off->after_extract_call = pt_instance_prop_offset(ce, "afterExtractCall", sizeof("afterExtractCall") - 1); + off->resolved_types = pt_instance_prop_offset(ce, "resolvedTypes", sizeof("resolvedTypes") - 1); + off->truthy_scopes = pt_instance_prop_offset(ce, "truthyScopes", sizeof("truthyScopes") - 1); + off->falsey_scopes = pt_instance_prop_offset(ce, "falseyScopes", sizeof("falseyScopes") - 1); + off->fiber_scope = pt_instance_prop_offset(ce, "fiberScope", sizeof("fiberScope") - 1); + off->scope_out_of_first_level = pt_instance_prop_offset(ce, "scopeOutOfFirstLevelStatement", sizeof("scopeOutOfFirstLevelStatement") - 1); + off->scope_with_promoted_native = pt_instance_prop_offset(ce, "scopeWithPromotedNativeTypes", sizeof("scopeWithPromotedNativeTypes") - 1); + off->mutating_scope = pt_instance_prop_offset(ce, "mutatingScope", sizeof("mutatingScope") - 1); + off->truthy_value_exprs = pt_instance_prop_offset(ce, "truthyValueExprs", sizeof("truthyValueExprs") - 1); + off->falsey_value_exprs = pt_instance_prop_offset(ce, "falseyValueExprs", sizeof("falseyValueExprs") - 1); + + zend_hash_add_ptr(&pt_scope_offsets_cache, ce->name, off); + return off; +} + +/* }}} */ + +namespace phpstanturbo { + +/* + * Mirrors PHPStan\Analyser\ScopeOps. Methods returning zv::Val use UNDEF to + * signal a pending exception; a legitimate PHP null is zv::Val::null(). + */ +class ScopeOps +{ +public: + /* Mirrors ScopeOps::nodeKey() (pt_node_key carries the key caching). */ + static zv::Val nodeKey(zend_object *node, zval *exprPrinter) + { + zend_string *key = pt_node_key(node, exprPrinter); + if (UNEXPECTED(key == NULL)) { + return zv::Val(); + } + return zv::Val::adoptString(key); + } + + /* + * Mirrors ScopeOps::getTypeFromCache(). *keyOut receives the computed key + * (owned by the caller) on both hit and miss, like the twin's + * unconditional `$key = ...` assignment; it stays NULL only when an + * exception is pending. A stored null counts as a miss — the twin's + * `?? null` cannot distinguish it from an absent entry either. + */ + static zv::Val getTypeFromCache(zval *scope, zend_object *node, zend_string **keyOut) + { + *keyOut = NULL; + + zval *exprPrinter = scopeProp(scope, "exprPrinter", sizeof("exprPrinter") - 1); + if (UNEXPECTED(exprPrinter == NULL)) { + return zv::Val(); + } + if (UNEXPECTED(Z_TYPE_P(exprPrinter) != IS_OBJECT)) { + zend_throw_error(NULL, "phpstan_turbo: exprPrinter is not an object"); + return zv::Val(); + } + + zv::Str key = zv::Str::adopt(pt_node_key(node, exprPrinter)); + if (UNEXPECTED(key.isNull())) { + return zv::Val(); + } + + zval *table = scopeProp(scope, "resolvedTypes", sizeof("resolvedTypes") - 1); + if (UNEXPECTED(table == NULL)) { + return zv::Val(); + } + if (EXPECTED(Z_TYPE_P(table) == IS_ARRAY)) { + zval *found = zend_symtable_find(Z_ARRVAL_P(table), key.get()); + if (found != NULL && Z_TYPE_P(found) != IS_NULL) { + zv::Val result = zv::Val::copyOf(zv::Ref(found)); + *keyOut = key.take(); + return result; + } + } + + *keyOut = key.take(); + return zv::Val::null(); + } + + /* + * Mirrors ScopeOps::expressionTypeByKey(): for non-Variable/Closure/ + * ArrowFunction nodes whose entry in expressionTypes has certainty Yes, + * returns the tracked type; null otherwise. + */ + static zv::Val expressionTypeByKey(zval *scope, zend_object *node, zend_string *exprString) + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + zend_class_entry *arrowFunctionCe = pt_class(PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(variableCe == NULL || closureCe == NULL || arrowFunctionCe == NULL)) { + return zv::Val(); + } + + if (instanceof_function(node->ce, variableCe) + || instanceof_function(node->ce, closureCe) + || instanceof_function(node->ce, arrowFunctionCe)) { + return zv::Val::null(); + } + + zval *table = scopeArrayProp(scope, "expressionTypes", sizeof("expressionTypes") - 1); + if (UNEXPECTED(table == NULL)) { + return zv::Val(); + } + zval *found = zend_symtable_find(Z_ARRVAL_P(table), exprString); + if (found == NULL) { + return zv::Val::null(); + } + zv::Ref holder = zv::Ref(found).deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + if (pt_holder_certainty_value(holder.asObject()) != PT_TRI_YES) { + return zv::Val::null(); + } + return zv::Val::copyOf(zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_TYPE)); + } + + /* Mirrors ScopeOps::hasExpressionType(). */ + static zv::Val hasExpressionType(zval *scope, zend_object *node, zval *exprPrinter) + { + pt_node_class_info *info = pt_get_node_class_info(node->ce); + if (UNEXPECTED(info == NULL)) { + return zv::Val(); + } + if (info->is_variable && info->name_offset >= 0) { + zv::Ref name = zv::ObjRef(node).propAtOffset((uint32_t) info->name_offset).deref(); + if (name.isString()) { + return hasVariableType(scope, name.asString()); + } + } + + zv::Str key = zv::Str::adopt(pt_node_key(node, exprPrinter)); + if (UNEXPECTED(key.isNull())) { + return zv::Val(); + } + zval *table = scopeArrayProp(scope, "expressionTypes", sizeof("expressionTypes") - 1); + if (UNEXPECTED(table == NULL)) { + return zv::Val(); + } + zval *found = zend_symtable_find(Z_ARRVAL_P(table), key.get()); + if (found == NULL) { + return trinarySingleton(PT_TRI_NO); + } + zv::Ref holder = zv::Ref(found).deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + return zv::Val::copyOf(zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_CERTAINTY)); + } + + /* Mirrors ScopeOps::hasVariableType(). */ + static zv::Val hasVariableType(zval *scope, zend_string *variableName) + { + if (pt_is_superglobal_name(variableName)) { + return trinarySingleton(PT_TRI_YES); + } + + zval *table = scopeProp(scope, "expressionTypes", sizeof("expressionTypes") - 1); + if (UNEXPECTED(table == NULL)) { + return zv::Val(); + } + if (EXPECTED(Z_TYPE_P(table) == IS_ARRAY)) { + zv::Str varKey = dollarPrefixed(variableName); + zval *found = zend_hash_find(Z_ARRVAL_P(table), varKey.get()); + if (found != NULL) { + zv::Ref holder = zv::Ref(found).deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + return zv::Val::copyOf(zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_CERTAINTY)); + } + } + + bool canExist; + if (UNEXPECTED(!pt_call_scope_bool(scope, "cananyvariableexist", sizeof("cananyvariableexist") - 1, 0, NULL, &canExist))) { + return zv::Val(); + } + return trinarySingleton(canExist ? PT_TRI_MAYBE : PT_TRI_NO); + } + + /* + * Mirrors ScopeOps::scopeWith(), but natively: the hot MutatingScope + * operations end in $this->scopeFactory->create() with ~30 arguments + * where only the expression tables and a couple of flags differ from the + * current scope. Cloning the scope and overwriting exactly those + * properties produces an identical object (the constructor is pure + * property promotion) while skipping the factory call, 30-argument + * dispatch and re-verification. Per-instance memo properties are reset + * to their fresh-constructor defaults. + */ + static zv::Val scopeWith( + zval *scope, + HashTable *expressionTypes, + HashTable *nativeExpressionTypes, + HashTable *conditionalExpressions, + HashTable *currentlyAssignedExpressions, + HashTable *currentlyAllowedUndefinedExpressions, + HashTable *inFunctionCallsStack, + bool inFirstLevelStatement, + bool afterExtractCall) + { + pt_scope_offsets *off = pt_scope_offsets_for(Z_OBJCE_P(scope)); + if (UNEXPECTED(off->expression_types < 0 || off->native_expression_types < 0 + || off->conditional_expressions < 0 || off->currently_assigned < 0 + || off->currently_allowed_undefined < 0 || off->in_function_calls_stack < 0 + || off->in_first_level_statement < 0 || off->after_extract_call < 0)) { + zend_throw_error(NULL, "phpstan_turbo: scope property layout mismatch"); + return zv::Val(); + } + + zend_object *clone = zend_objects_clone_obj(Z_OBJ_P(scope)); + if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + zv::ObjRef cloneObj(clone); + + setTableProp(cloneObj, off->expression_types, expressionTypes); + setTableProp(cloneObj, off->native_expression_types, nativeExpressionTypes); + setTableProp(cloneObj, off->conditional_expressions, conditionalExpressions); + setTableProp(cloneObj, off->currently_assigned, currentlyAssignedExpressions); + setTableProp(cloneObj, off->currently_allowed_undefined, currentlyAllowedUndefinedExpressions); + setTableProp(cloneObj, off->in_function_calls_stack, inFunctionCallsStack); + cloneObj.propAtOffset((uint32_t) off->in_first_level_statement).assign(zv::Val::boolean(inFirstLevelStatement)); + cloneObj.propAtOffset((uint32_t) off->after_extract_call).assign(zv::Val::boolean(afterExtractCall)); + + /* fresh-constructor defaults for per-instance memos */ + resetToEmptyArray(cloneObj, off->resolved_types); + resetToEmptyArray(cloneObj, off->truthy_scopes); + resetToEmptyArray(cloneObj, off->falsey_scopes); + resetToNull(cloneObj, off->fiber_scope); + resetToNull(cloneObj, off->scope_out_of_first_level); + resetToNull(cloneObj, off->scope_with_promoted_native); + resetToNull(cloneObj, off->mutating_scope); + resetToEmptyArray(cloneObj, off->truthy_value_exprs); + resetToEmptyArray(cloneObj, off->falsey_value_exprs); + + zval out; + ZVAL_OBJ(&out, clone); + return zv::Val::adopt(out); + } + + /* Mirrors ScopeOps::mergeVariableHolders(). */ + static zv::Val mergeVariableHolders(zv::TableRef ours, zv::TableRef theirs) + { + zv::Arr merged = zv::Arr::create(ours.size()); + if (UNEXPECTED(!mergeVariableHoldersInto(merged, ours, theirs))) { + return zv::Val(); + } + return merged; + } + + /* Mirrors ScopeOps::finishMerge(). Returns [filteredMerged, mergedNative]. */ + static zv::Val finishMerge(zv::TableRef merged, zv::TableRef oursExpr, zv::TableRef theirsExpr, zv::TableRef oursNative, zv::TableRef theirsNative) + { + zv::Arr filteredMerged; + if (UNEXPECTED(!filterHolders(merged, filteredMerged))) { + return zv::Val(); + } + + zv::Arr oursNativeRemaining = zv::Arr::adoptTable(zend_array_dup(oursNative.table())); + zv::Arr theirsNativeRemaining = zv::Arr::adoptTable(zend_array_dup(theirsNative.table())); + zv::Arr mergedNative = zv::Arr::create(0); + + for (auto entry : oursNative) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holder = entry.value().deref(); + + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + + zval *theirNativeSlot = pt_ht_find(theirsNative.table(), key, idx); + if (theirNativeSlot == NULL) { + continue; + } + zval *ourExprSlot = pt_ht_find(oursExpr.table(), key, idx); + if (ourExprSlot == NULL) { + continue; + } + zval *theirExprSlot = pt_ht_find(theirsExpr.table(), key, idx); + if (theirExprSlot == NULL) { + continue; + } + + bool equal; + { + zv::Ref ourExprHolder = zv::Ref(ourExprSlot).deref(); + if (UNEXPECTED(!pt_check_holder(ourExprHolder.raw()))) { + return zv::Val(); + } + if (UNEXPECTED(!pt_holder_equals(holder.raw(), ourExprHolder.raw(), &equal))) { + return zv::Val(); + } + } + if (!equal) { + continue; + } + { + zv::Ref theirNativeHolder = zv::Ref(theirNativeSlot).deref(); + zv::Ref theirExprHolder = zv::Ref(theirExprSlot).deref(); + if (UNEXPECTED(!pt_check_holder(theirNativeHolder.raw())) || UNEXPECTED(!pt_check_holder(theirExprHolder.raw()))) { + return zv::Val(); + } + if (UNEXPECTED(!pt_holder_equals(theirNativeHolder.raw(), theirExprHolder.raw(), &equal))) { + return zv::Val(); + } + } + if (!equal) { + continue; + } + + zval *mergedHolder = pt_ht_find(filteredMerged.table(), key, idx); + if (mergedHolder == NULL) { + continue; + } + + tableUpdateCopy(mergedNative.table(), key, idx, zv::Ref(mergedHolder)); + pt_ht_del(oursNativeRemaining.table(), key, idx); + pt_ht_del(theirsNativeRemaining.table(), key, idx); + } + + /* mergedNative += filter(mergeVariableHolders(oursRemaining, theirsRemaining)) */ + { + zv::Val remainingMerged = mergeVariableHolders(zv::TableRef(oursNativeRemaining.table()), zv::TableRef(theirsNativeRemaining.table())); + if (UNEXPECTED(remainingMerged.isUndef())) { + return zv::Val(); + } + zv::Arr remainingFiltered; + if (UNEXPECTED(!filterHolders(zv::TableRef(Z_ARRVAL_P(remainingMerged.raw())), remainingFiltered))) { + return zv::Val(); + } + for (auto entry : zv::ArrRef(remainingFiltered.raw())) { + tableUpdateCopy(mergedNative.table(), entry.stringKeyOrNull(), entry.indexKey(), entry.value()); + } + } + + zv::Arr result = zv::Arr::create(2); + result.push(std::move(filteredMerged)); + result.push(std::move(mergedNative)); + return result; + } + + /* Mirrors ScopeOps::intersectConditionalExpressions(). */ + static zv::Val intersectConditionalExpressions(zv::TableRef ours, zv::TableRef theirs) + { + zv::Arr result = zv::Arr::create(0); + + for (auto entry : ours) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + + zval *otherHoldersSlot = pt_ht_find(theirs.table(), key, idx); + if (otherHoldersSlot == NULL) { + continue; + } + zv::Ref holders = entry.value().deref(); + zv::Ref otherHolders = zv::Ref(otherHoldersSlot).deref(); + if (!holders.isArray() || !otherHolders.isArray()) { + continue; + } + HashTable *otherTable = otherHolders.asArrayTable(); + + zv::Arr intersected; /* stays UNDEF until the first shared holder */ + for (auto holderEntry : zv::TableRef(holders.asArrayTable())) { + zend_string *holderKey = holderEntry.stringKeyOrNull(); + zend_ulong holderIdx = holderEntry.indexKey(); + if (!pt_ht_exists(otherTable, holderKey, holderIdx)) { + continue; + } + if (intersected.isUndef()) { + intersected = zv::Arr::create(0); + } + tableAddNewCopy(intersected.table(), holderKey, holderIdx, holderEntry.value()); + } + + if (intersected.isUndef()) { + continue; + } + tableAddNew(result.table(), key, idx, std::move(intersected)); + } + + return result; + } + + /* + * Mirrors ScopeOps::createConditionalExpressions(). The isSuperTypeOf / + * isConstantArray results are cached per guard, exactly like the twin's + * per-guard caches; the input array is only duplicated once the first + * conditional is actually appended. + */ + static zv::Val createConditionalExpressions(zv::TableRef conditional, zv::TableRef ours, zv::TableRef theirs, zv::TableRef merged) + { + zend_class_entry *virtualNodeCe = pt_class(PT_CLASS_VIRTUAL_NODE); + if (UNEXPECTED(virtualNodeCe == NULL)) { + return zv::Val(); + } + + zv::ScratchTable guardsToExclude(8); + zv::ScratchTable typeGuards(8); + + /* guardsToExclude: subtype-absorbed their-branch variables are poor + * guards but stay valid conditional targets */ + for (auto entry : theirs) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + + zval *mergedSlot = pt_ht_find(merged.table(), key, idx); + if (mergedSlot == NULL) { + continue; + } + zv::Ref holder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + bool equalTypes; + { + zv::Ref mergedHolder = zv::Ref(mergedSlot).deref(); + if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) { + return zv::Val(); + } + if (UNEXPECTED(!pt_holder_equal_types(mergedHolder.raw(), holder.raw(), &equalTypes))) { + return zv::Val(); + } + } + if (!equalTypes) { + continue; + } + + zval *ourSlot = pt_ht_find(ours.table(), key, idx); + if (ourSlot != NULL) { + zv::Ref ourHolder = zv::Ref(ourSlot).deref(); + if (UNEXPECTED(!pt_check_holder(ourHolder.raw()))) { + return zv::Val(); + } + if (pt_holder_certainty_value(ourHolder.asObject()) != pt_holder_certainty_value(holder.asObject())) { + bool ourEqualTypes; + if (UNEXPECTED(!pt_holder_equal_types(ourHolder.raw(), holder.raw(), &ourEqualTypes))) { + return zv::Val(); + } + if (ourEqualTypes) { + continue; + } + } + } + + zval trueZv; + ZVAL_TRUE(&trueZv); + pt_ht_update(guardsToExclude.table(), key, idx, &trueZv); + } + + /* typeGuards */ + for (auto entry : ours) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holder = entry.value().deref(); + + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + if (instanceof_function(holderExpr(holder)->ce, virtualNodeCe)) { + continue; + } + zval *mergedSlot = pt_ht_find(merged.table(), key, idx); + if (mergedSlot == NULL) { + continue; + } + if (pt_holder_certainty_value(holder.asObject()) != PT_TRI_YES) { + continue; + } + if (pt_ht_exists(guardsToExclude.table(), key, idx)) { + continue; + } + zval *theirSlot = pt_ht_find(theirs.table(), key, idx); + if (theirSlot != NULL) { + zv::Ref theirHolder = zv::Ref(theirSlot).deref(); + if (UNEXPECTED(!pt_check_holder(theirHolder.raw()))) { + return zv::Val(); + } + if (pt_holder_certainty_value(theirHolder.asObject()) != PT_TRI_YES) { + continue; + } + } + bool equalTypes; + { + zv::Ref mergedHolder = zv::Ref(mergedSlot).deref(); + if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) { + return zv::Val(); + } + if (UNEXPECTED(!pt_holder_equal_types(mergedHolder.raw(), holder.raw(), &equalTypes))) { + return zv::Val(); + } + } + if (equalTypes) { + continue; + } + + /* borrowed entry — the scratch table has no destructor */ + zval borrowed; + ZVAL_COPY_VALUE(&borrowed, holder.raw()); + pt_ht_update(typeGuards.table(), key, idx, &borrowed); + } + + if (typeGuards.size() == 0) { + return zv::Arr::copyOfTable(conditional.table()); + } + + /* Both isSuperTypeOf() results depend only on the guard, not on the + * target expression — cache them per guard across the target loop. */ + zv::ScratchTable guardIsSuperTypeOfTheirExprCache(8); + zv::ScratchTable theirExprIsSuperTypeOfGuardCache(8); + zv::ScratchTable guardIsConstantArrayCache(8); + zv::Arr result; /* stays UNDEF until the first append duplicates the input */ + + /* main loop: pair non-merged expressions with guards */ + for (auto entry : ours) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holder = entry.value().deref(); + + if (instanceof_function(holderExpr(holder)->ce, virtualNodeCe)) { + continue; + } + zval *mergedSlot = pt_ht_find(merged.table(), key, idx); + if (mergedSlot != NULL) { + zv::Ref mergedHolder = zv::Ref(mergedSlot).deref(); + bool equal; + if (UNEXPECTED(!pt_holder_equals(mergedHolder.raw(), holder.raw(), &equal))) { + return zv::Val(); + } + if (equal) { + continue; + } + } + + bool hasSelfGuard = pt_ht_exists(typeGuards.table(), key, idx); + if (typeGuards.size() - (hasSelfGuard ? 1 : 0) == 0) { + continue; + } + bool exprIsGuardExcluded = pt_ht_exists(guardsToExclude.table(), key, idx); + + for (auto guardEntry : zv::TableRef(typeGuards.table())) { + zend_string *guardKey = guardEntry.stringKeyOrNull(); + zend_ulong guardIdx = guardEntry.indexKey(); + zv::Ref guardHolder = guardEntry.value(); + + if (sameDualKey(guardKey, guardIdx, key, idx)) { + continue; + } + + if (exprIsGuardExcluded) { + /* a subtype-absorbed target paired with a constant-array + * guard never helps — skip before pricing supertype checks */ + zval *cached = pt_ht_find(guardIsConstantArrayCache.table(), guardKey, guardIdx); + bool isConstantArray; + if (cached != NULL) { + isConstantArray = Z_TYPE_P(cached) == IS_TRUE; + } else { + if (UNEXPECTED(!isConstantArrayYes(holderType(guardHolder), &isConstantArray))) { + return zv::Val(); + } + zval cacheVal; + ZVAL_BOOL(&cacheVal, isConstantArray); + pt_ht_update(guardIsConstantArrayCache.table(), guardKey, guardIdx, &cacheVal); + } + if (isConstantArray) { + continue; + } + } + + zval *theirGuardSlot = pt_ht_find(theirs.table(), guardKey, guardIdx); + if (theirGuardSlot != NULL) { + zv::Ref theirGuard = zv::Ref(theirGuardSlot).deref(); + if (pt_holder_certainty_value(theirGuard.asObject()) == PT_TRI_YES) { + zend_long guardIsSuperTypeOfTheirExpr; + zval *cached = pt_ht_find(guardIsSuperTypeOfTheirExprCache.table(), guardKey, guardIdx); + if (cached != NULL) { + guardIsSuperTypeOfTheirExpr = Z_LVAL_P(cached); + } else { + if (UNEXPECTED(!isSuperTypeOfValue(holderType(guardHolder), holderType(theirGuard), &guardIsSuperTypeOfTheirExpr))) { + return zv::Val(); + } + zval cacheVal; + ZVAL_LONG(&cacheVal, guardIsSuperTypeOfTheirExpr); + pt_ht_update(guardIsSuperTypeOfTheirExprCache.table(), guardKey, guardIdx, &cacheVal); + } + + if (guardIsSuperTypeOfTheirExpr == PT_TRI_YES) { + continue; + } + + bool skip = false; + zval *theirExprSlot = pt_ht_find(theirs.table(), key, idx); + if (theirExprSlot != NULL) { + zv::Ref theirExprHolder = zv::Ref(theirExprSlot).deref(); + if (pt_holder_certainty_value(theirExprHolder.asObject()) == PT_TRI_YES && guardIsSuperTypeOfTheirExpr != PT_TRI_NO) { + skip = true; + } + } else if (guardIsSuperTypeOfTheirExpr != PT_TRI_NO) { + bool typesEqual = pt_types_identical_or_equal(holderType(holder), holderType(guardHolder)); + if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + if (typesEqual) { + skip = true; + } + } + + /* the reverse isSuperTypeOf() is priced last, only + * when the cheaper conditions did not already decide */ + if (!skip) { + zend_long theirExprIsSuperTypeOfGuard; + zval *cachedReverse = pt_ht_find(theirExprIsSuperTypeOfGuardCache.table(), guardKey, guardIdx); + if (cachedReverse != NULL) { + theirExprIsSuperTypeOfGuard = Z_LVAL_P(cachedReverse); + } else { + if (UNEXPECTED(!isSuperTypeOfValue(holderType(theirGuard), holderType(guardHolder), &theirExprIsSuperTypeOfGuard))) { + return zv::Val(); + } + zval cacheVal; + ZVAL_LONG(&cacheVal, theirExprIsSuperTypeOfGuard); + pt_ht_update(theirExprIsSuperTypeOfGuardCache.table(), guardKey, guardIdx, &cacheVal); + } + if (theirExprIsSuperTypeOfGuard == PT_TRI_YES) { + skip = true; + } + } + + if (skip) { + continue; + } + } + } + + if (UNEXPECTED(!appendConditional(result, conditional, key, idx, guardKey, guardIdx, guardHolder, holder))) { + return zv::Val(); + } + } + } + + /* their-only expressions: record certainty-No conditionals per guard */ + for (auto entry : merged) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + + if (pt_ht_exists(ours.table(), key, idx)) { + continue; + } + zv::Ref mergedHolder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(mergedHolder.raw()))) { + return zv::Val(); + } + + for (auto guardEntry : zv::TableRef(typeGuards.table())) { + zv::Val noHolder = createNoErrorHolder(zv::ObjRef(mergedHolder.asObject()).propAt(PT_ETH_PROP_EXPR).raw()); + if (UNEXPECTED(noHolder.isUndef())) { + return zv::Val(); + } + if (UNEXPECTED(!appendConditional(result, conditional, key, idx, guardEntry.stringKeyOrNull(), guardEntry.indexKey(), guardEntry.value(), noHolder.ref()))) { + return zv::Val(); + } + } + } + + if (!result.isUndef()) { + return result; + } + return zv::Arr::copyOfTable(conditional.table()); + } + + /* + * Mirrors ScopeOps::invalidateExpressionEntries(). Returns + * [expressionTypes, nativeExpressionTypes, conditionalExpressions] with + * the invalidated entries removed, or null when nothing changed; the + * expression tables are only duplicated once an entry is invalidated. + */ + static zv::Val invalidateExpressionEntries( + zval *scope, + zval *exprPrinter, + zend_string *exprStringToInvalidate, + zval *expressionToInvalidate, + bool requireMoreCharacters, + zval *invalidatingClass, + zv::TableRef expressionTypes, + zv::TableRef nativeExpressionTypes, + zv::TableRef conditionalExpressions) + { + InvalidationQuery query = { + scope, + exprPrinter, + exprStringToInvalidate, + expressionToInvalidate, + invalidatingClass, + zend_string_equals_literal(exprStringToInvalidate, "$this"), + }; + + bool invalidated = false; + zv::Arr resultExpr, resultNative; /* stay UNDEF until the first hit */ + + for (auto entry : expressionTypes) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holder = entry.value().deref(); + + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + zend_object *expr = holderExpr(holder); + zend_string *entryKey = key != NULL ? key : zend_long_to_str((zend_long) idx); + bool failed = false; + bool should = shouldInvalidate(query, entryKey, expr, requireMoreCharacters, &failed); + if (key == NULL) { + zend_string_release(entryKey); + } + if (!should) { + if (UNEXPECTED(failed)) { + return zv::Val(); + } + continue; + } + if (resultExpr.isUndef()) { + resultExpr = zv::Arr::adoptTable(zend_array_dup(expressionTypes.table())); + resultNative = zv::Arr::adoptTable(zend_array_dup(nativeExpressionTypes.table())); + } + pt_ht_del(resultExpr.table(), key, idx); + pt_ht_del(resultNative.table(), key, idx); + invalidated = true; + } + + zv::Arr resultConditional = zv::Arr::create(0); + for (auto entry : conditionalExpressions) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holders = entry.value().deref(); + + if (!holders.isArray()) { + continue; + } + zv::TableRef holdersTable(holders.asArrayTable()); + if (holdersTable.size() == 0) { + continue; + } + + /* first holder's type-holder expr decides whole-group invalidation */ + { + zv::Ref firstHolder = (*holdersTable.begin()).value().deref(); + if (UNEXPECTED(!firstHolder.instanceOf(pt_ce_cond_expr_holder))) { + zend_type_error("phpstan_turbo: expected ConditionalExpressionHolder"); + return zv::Val(); + } + zend_object *firstExpr = holderExpr(zv::ObjRef(firstHolder.asObject()).propAt(PT_CEH_PROP_TYPEHOLDER)); + zv::Str firstKey = zv::Str::adopt(pt_node_key(firstExpr, exprPrinter)); + if (UNEXPECTED(firstKey.isNull())) { + return zv::Val(); + } + bool failed = false; + bool drop = shouldInvalidate(query, firstKey.get(), firstExpr, requireMoreCharacters, &failed); + if (UNEXPECTED(failed)) { + return zv::Val(); + } + if (drop) { + invalidated = true; + continue; + } + } + + zv::Arr filtered; /* stays UNDEF until the first kept holder */ + for (auto holderEntry : holdersTable) { + zv::Ref holder = holderEntry.value().deref(); + if (UNEXPECTED(!holder.instanceOf(pt_ce_cond_expr_holder))) { + zend_type_error("phpstan_turbo: expected ConditionalExpressionHolder"); + return zv::Val(); + } + bool keep = true; + zv::Ref conditions = zv::ObjRef(holder.asObject()).propAt(PT_CEH_PROP_CONDS); + if (conditions.isArray()) { + for (auto conditionEntry : zv::TableRef(conditions.asArrayTable())) { + zv::Ref conditionHolder = conditionEntry.value().deref(); + if (UNEXPECTED(!pt_check_holder(conditionHolder.raw()))) { + return zv::Val(); + } + zend_object *conditionExpr = holderExpr(conditionHolder); + zend_string *conditionKey = conditionEntry.stringKeyOrNull(); + zend_string *conditionKeyStr = conditionKey != NULL ? conditionKey : zend_long_to_str((zend_long) conditionEntry.indexKey()); + bool failed = false; + bool should = shouldInvalidate(query, conditionKeyStr, conditionExpr, false, &failed); + if (conditionKey == NULL) { + zend_string_release(conditionKeyStr); + } + if (should) { + invalidated = true; + keep = false; + break; + } + if (UNEXPECTED(failed)) { + return zv::Val(); + } + } + } + if (!keep) { + continue; + } + if (filtered.isUndef()) { + filtered = zv::Arr::create(0); + } + tableAddNewCopy(filtered.table(), holderEntry.stringKeyOrNull(), holderEntry.indexKey(), holder); + } + + if (filtered.isUndef()) { + continue; + } + tableAddNew(resultConditional.table(), key, idx, std::move(filtered)); + } + + if (!invalidated) { + return zv::Val::null(); + } + + if (resultExpr.isUndef()) { + /* only conditional expressions were invalidated */ + resultExpr = zv::Arr::adoptTable(zend_array_dup(expressionTypes.table())); + resultNative = zv::Arr::adoptTable(zend_array_dup(nativeExpressionTypes.table())); + } + + zv::Arr result = zv::Arr::create(3); + result.push(std::move(resultExpr)); + result.push(std::move(resultNative)); + result.push(std::move(resultConditional)); + return result; + } + + /* + * Mirrors ScopeOps::invalidateMethodsOnExpression(): drops tracked + * MethodCall expressions whose var matches the invalidated key. Returns + * null when nothing changed. + */ + static zv::Val invalidateMethodsOnExpression(zval *exprPrinter, zend_string *exprStringToInvalidate, zv::TableRef expressionTypes, zv::TableRef nativeExpressionTypes) + { + zend_class_entry *methodCallCe = pt_class(PT_CLASS_METHOD_CALL); + if (UNEXPECTED(methodCallCe == NULL)) { + return zv::Val(); + } + + bool invalidated = false; + zv::Arr resultExpr, resultNative; /* stay UNDEF until the first hit */ + + for (auto entry : expressionTypes) { + zv::Ref holder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return zv::Val(); + } + zend_object *expr = holderExpr(holder); + if (!instanceof_function(expr->ce, methodCallCe)) { + continue; + } + int32_t varOffset = pt_instance_prop_offset(expr->ce, "var", sizeof("var") - 1); + if (varOffset < 0) { + continue; + } + zv::Ref var = zv::ObjRef(expr).propAtOffset((uint32_t) varOffset).deref(); + if (!var.isObject()) { + continue; + } + zv::Str varKey = zv::Str::adopt(pt_node_key(var.asObject(), exprPrinter)); + if (UNEXPECTED(varKey.isNull())) { + return zv::Val(); + } + if (!zend_string_equals(varKey.get(), exprStringToInvalidate)) { + continue; + } + + if (resultExpr.isUndef()) { + resultExpr = zv::Arr::adoptTable(zend_array_dup(expressionTypes.table())); + resultNative = zv::Arr::adoptTable(zend_array_dup(nativeExpressionTypes.table())); + } + pt_ht_del(resultExpr.table(), entry.stringKeyOrNull(), entry.indexKey()); + pt_ht_del(resultNative.table(), entry.stringKeyOrNull(), entry.indexKey()); + invalidated = true; + } + + if (!invalidated) { + return zv::Val::null(); + } + + zv::Arr result = zv::Arr::create(2); + result.push(std::move(resultExpr)); + result.push(std::move(resultNative)); + return result; + } + + /* Mirrors ScopeOps::shouldInvalidateExpression(). */ + static bool shouldInvalidateExpression(zval *scope, zval *exprPrinter, zend_string *exprStringToInvalidate, zval *exprToInvalidate, zend_object *expr, zend_string *exprString, bool requireMoreCharacters, zval *invalidatingClass, bool *failed) + { + InvalidationQuery query = { + scope, + exprPrinter, + exprStringToInvalidate, + exprToInvalidate, + invalidatingClass, + zend_string_equals_literal(exprStringToInvalidate, "$this"), + }; + return shouldInvalidate(query, exprString, expr, requireMoreCharacters, failed); + } + + /* Mirrors ScopeOps::getIntertwinedRefRootVariableName(). */ + static zv::Val getIntertwinedRefRootVariableName(zend_object *expr) + { + zend_string *name = intertwinedRootVariableName(expr); + if (name != NULL) { + /* borrowed (points into a property) — copy for the caller */ + return zv::Val::string(name); + } + if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + return zv::Val::null(); + } + + /* + * Mirrors ScopeOps::buildTypeSpecifications(); the usort becomes a qsort + * over a flat working set, kept stable via a sequence number. + */ + static zv::Val buildTypeSpecifications(zv::TableRef sureTypes, zv::TableRef sureNotTypes) + { + uint32_t capacity = sureTypes.size() + sureNotTypes.size(); + TypeSpec *specs = capacity > 0 ? (TypeSpec *) emalloc(capacity * sizeof(TypeSpec)) : NULL; + uint32_t count = 0; + + if (UNEXPECTED(!collectTypeSpecifications(sureTypes, true, specs, &count)) + || UNEXPECTED(!collectTypeSpecifications(sureNotTypes, false, specs, &count))) { + for (uint32_t i = 0; i < count; i++) { + zend_string_release(specs[i].exprString); + } + if (specs != NULL) { + efree(specs); + } + return zv::Val(); + } + + if (count > 1) { + qsort(specs, count, sizeof(TypeSpec), compareTypeSpecifications); + } + + zv::Arr result = zv::Arr::create(count); + for (uint32_t i = 0; i < count; i++) { + zv::Arr item = zv::Arr::create(4); + item.set("sure", zv::Val::boolean(specs[i].sure)); + item.set("exprString", zv::Val::adoptString(specs[i].exprString)); + item.set("expr", zv::Val::copyOf(zv::Ref(&specs[i].expr))); + item.set("type", zv::Val::copyOf(zv::Ref(&specs[i].type))); + result.push(std::move(item)); + } + + if (specs != NULL) { + efree(specs); + } + return result; + } + + /* + * Mirrors ScopeOps::matchConditionalExpressions(): the fixed-point loop + * with an exact-match pass and a supertype-match pass per expression. + * Returns [conditions, specifiedExpressions]. + */ + static zv::Val matchConditionalExpressions(zv::TableRef conditionalExpressions, zv::TableRef specifiedInput) + { + zv::Arr conditions = zv::Arr::create(0); + zv::Arr specified = zv::Arr::adoptTable(zend_array_dup(specifiedInput.table())); + uint32_t previousCount = UINT32_MAX; + + while (zend_hash_num_elements(specified.table()) != previousCount) { + previousCount = zend_hash_num_elements(specified.table()); + + for (auto entry : conditionalExpressions) { + zend_string *conditionalKey = entry.stringKeyOrNull(); + zend_ulong conditionalIdx = entry.indexKey(); + + if (pt_ht_exists(conditions.table(), conditionalKey, conditionalIdx)) { + continue; + } + zv::Ref holders = entry.value().deref(); + if (UNEXPECTED(!holders.isArray())) { + continue; + } + zv::TableRef holdersTable(holders.asArrayTable()); + + /* Pass 1: prefer exact matches */ + for (auto holderEntry : holdersTable) { + zv::Ref holder = holderEntry.value().deref(); + if (UNEXPECTED(!holder.instanceOf(pt_ce_cond_expr_holder))) { + zend_type_error("phpstan_turbo: expected ConditionalExpressionHolder"); + return zv::Val(); + } + zv::Ref typeHolder = zv::ObjRef(holder.asObject()).propAt(PT_CEH_PROP_TYPEHOLDER); + if (pt_holder_certainty_value(typeHolder.asObject()) == PT_TRI_NO + && pt_ht_exists(specifiedInput.table(), conditionalKey, conditionalIdx)) { + continue; + } + zv::Ref conditionHolders = zv::ObjRef(holder.asObject()).propAt(PT_CEH_PROP_CONDS); + if (UNEXPECTED(!conditionHolders.isArray())) { + continue; + } + bool all = true; + for (auto conditionEntry : zv::TableRef(conditionHolders.asArrayTable())) { + zval *specifiedSlot = pt_ht_find(specified.table(), conditionEntry.stringKeyOrNull(), conditionEntry.indexKey()); + if (specifiedSlot == NULL) { + all = false; + break; + } + zv::Ref conditionHolder = conditionEntry.value().deref(); + zv::Ref specifiedHolder = zv::Ref(specifiedSlot).deref(); + if (UNEXPECTED(!pt_check_holder(conditionHolder.raw())) || UNEXPECTED(!pt_check_holder(specifiedHolder.raw()))) { + return zv::Val(); + } + bool equal; + if (UNEXPECTED(!pt_holder_equals(conditionHolder.raw(), specifiedHolder.raw(), &equal))) { + return zv::Val(); + } + if (!equal) { + all = false; + break; + } + } + if (!all) { + continue; + } + + recordMatchedCondition(conditions, specified, conditionalKey, conditionalIdx, holder, typeHolder); + } + + if (pt_ht_exists(conditions.table(), conditionalKey, conditionalIdx)) { + continue; + } + + /* Pass 2: supertype match, only when Pass 1 found nothing */ + for (auto holderEntry : holdersTable) { + zv::Ref holder = holderEntry.value().deref(); + zv::Ref typeHolder = zv::ObjRef(holder.asObject()).propAt(PT_CEH_PROP_TYPEHOLDER); + if (pt_holder_certainty_value(typeHolder.asObject()) == PT_TRI_NO) { + continue; + } + zv::Ref conditionHolders = zv::ObjRef(holder.asObject()).propAt(PT_CEH_PROP_CONDS); + if (UNEXPECTED(!conditionHolders.isArray())) { + continue; + } + bool all = true; + for (auto conditionEntry : zv::TableRef(conditionHolders.asArrayTable())) { + zval *specifiedSlot = pt_ht_find(specified.table(), conditionEntry.stringKeyOrNull(), conditionEntry.indexKey()); + if (specifiedSlot == NULL) { + all = false; + break; + } + zv::Ref conditionHolder = conditionEntry.value().deref(); + zv::Ref specifiedHolder = zv::Ref(specifiedSlot).deref(); + /* Pass 1 validates only the entries it reaches before + * its first mismatch, so these can be unchecked here; + * the twin raises a catchable Error on wrong types */ + if (UNEXPECTED(!pt_check_holder(conditionHolder.raw()) || !pt_check_holder(specifiedHolder.raw()))) { + return zv::Val(); + } + if (pt_holder_certainty_value(conditionHolder.asObject()) != pt_holder_certainty_value(specifiedHolder.asObject())) { + all = false; + break; + } + zend_long superTypeOf; + if (UNEXPECTED(!isSuperTypeOfValue(holderType(conditionHolder), holderType(specifiedHolder), &superTypeOf))) { + return zv::Val(); + } + if (superTypeOf != PT_TRI_YES) { + all = false; + break; + } + } + if (!all) { + continue; + } + + recordMatchedCondition(conditions, specified, conditionalKey, conditionalIdx, holder, typeHolder); + } + } + } + + zv::Arr result = zv::Arr::create(2); + result.push(std::move(conditions)); + result.push(std::move(specified)); + return result; + } + +private: + /* Everything shouldInvalidate() needs to know about one invalidation. */ + struct InvalidationQuery + { + zval *scope; + zval *exprPrinter; + zend_string *exprStringToInvalidate; + zval *expressionToInvalidate; + zval *invalidatingClass; /* may be NULL */ + bool isThis; + }; + + /* One row of buildTypeSpecifications()' sortable working set. */ + struct TypeSpec + { + zend_string *exprString; /* owned */ + zval expr; /* borrowed */ + zval type; /* borrowed */ + bool sure; + uint32_t seq; + }; + + static zv::Val trinarySingleton(zend_long value) + { + return zv::Val::copyOf(zv::Ref(pt_trinary_singleton(value))); + } + + /* Scope property slot by name, deref'd; NULL + throw when missing. */ + static zval *scopeProp(zval *scope, const char *name, size_t len) + { + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&Z_OBJCE_P(scope)->properties_info, name, len); + if (UNEXPECTED(info == NULL || (info->flags & ZEND_ACC_STATIC) != 0)) { + zend_throw_error(NULL, "phpstan_turbo: %s property not found", name); + return NULL; + } + zval *slot = OBJ_PROP(Z_OBJ_P(scope), info->offset); + ZVAL_DEREF(slot); + return slot; + } + + /* Like scopeProp(), but the property must hold an array. */ + static zval *scopeArrayProp(zval *scope, const char *name, size_t len) + { + zval *table = scopeProp(scope, name, len); + if (UNEXPECTED(table == NULL)) { + return NULL; + } + if (UNEXPECTED(Z_TYPE_P(table) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: %s is not an array", name); + return NULL; + } + return table; + } + + /* '$' . $name */ + static zv::Str dollarPrefixed(zend_string *name) + { + zend_string *key = zend_string_alloc(ZSTR_LEN(name) + 1, 0); + ZSTR_VAL(key)[0] = '$'; + memcpy(ZSTR_VAL(key) + 1, ZSTR_VAL(name), ZSTR_LEN(name)); + ZSTR_VAL(key)[ZSTR_LEN(key)] = '\0'; + return zv::Str::adopt(key); + } + + /* $table[$key] = $value for a key known absent (addref-copy) */ + static void tableAddNewCopy(HashTable *table, zend_string *skey, zend_ulong idx, zv::Ref value) + { + Z_TRY_ADDREF_P(value.raw()); + pt_ht_add_new(table, skey, idx, value.raw()); + } + + /* $table[$key] = $value for a key known absent (consumes the value) */ + static void tableAddNew(HashTable *table, zend_string *skey, zend_ulong idx, zv::Val value) + { + zval v = value.take(); + pt_ht_add_new(table, skey, idx, &v); + } + + /* $table[$key] = $value (addref-copy, overwrites) */ + static void tableUpdateCopy(HashTable *table, zend_string *skey, zend_ulong idx, zv::Ref value) + { + Z_TRY_ADDREF_P(value.raw()); + pt_ht_update(table, skey, idx, value.raw()); + } + + /* $obj->prop = $table (addref-copy; immutable [] handled by copyOfTable) */ + static void setTableProp(zv::ObjRef obj, int32_t offset, HashTable *value) + { + obj.propAtOffset((uint32_t) offset).assign(zv::Arr::copyOfTable(value)); + } + + /* $obj->prop = [] — a memo reset to the fresh-constructor default */ + static void resetToEmptyArray(zv::ObjRef obj, int32_t offset) + { + if (offset < 0) { + return; + } + obj.propAtOffset((uint32_t) offset).assign(zv::Arr::empty()); + } + + /* $obj->prop = null — a memo reset to the fresh-constructor default */ + static void resetToNull(zv::ObjRef obj, int32_t offset) + { + if (offset < 0) { + return; + } + obj.propAtOffset((uint32_t) offset).assign(zv::Val::null()); + } + + /* $holder->expr for a checked ExpressionTypeHolder */ + static zend_object *holderExpr(zv::Ref holder) + { + return zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_EXPR).asObject(); + } + + /* &$holder->type slot for a checked ExpressionTypeHolder */ + static zval *holderType(zv::Ref holder) + { + return zv::ObjRef(holder.asObject()).propAt(PT_ETH_PROP_TYPE).raw(); + } + + static bool sameDualKey(zend_string *aKey, zend_ulong aIdx, zend_string *bKey, zend_ulong bIdx) + { + return aKey != NULL ? (bKey != NULL && zend_string_equals(aKey, bKey)) : (bKey == NULL && aIdx == bIdx); + } + + /* ExpressionTypeHolder::createMaybe($holder->expr, $holder->type) */ + static zv::Val createMaybeHolder(zv::Ref holder) + { + zv::ObjRef holderObj(holder.asObject()); + zval created; + pt_holder_create(&created, holderObj.propAt(PT_ETH_PROP_EXPR).raw(), holderObj.propAt(PT_ETH_PROP_TYPE).raw(), PT_TRI_MAYBE); + return zv::Val::adopt(created); + } + + /* The two loops of mergeVariableHolders(), filling a caller-owned table. */ + static bool mergeVariableHoldersInto(zv::Arr &merged, zv::TableRef ours, zv::TableRef theirs) + { + for (auto entry : ours) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + zv::Ref holder = entry.value().deref(); + + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return false; + } + + zval *theirSlot = pt_ht_find(theirs.table(), key, idx); + if (theirSlot != NULL) { + zv::Ref theirHolder = zv::Ref(theirSlot).deref(); + if (UNEXPECTED(!pt_check_holder(theirHolder.raw()))) { + return false; + } + if (holder.asObject() == theirHolder.asObject()) { + tableAddNewCopy(merged.table(), key, idx, holder); + } else { + zval andHolder; + if (UNEXPECTED(!pt_holder_and(holder.raw(), theirHolder.raw(), &andHolder))) { + return false; + } + tableAddNew(merged.table(), key, idx, zv::Val::adopt(andHolder)); + } + } else { + bool containsSuperGlobal = pt_expr_contains_superglobal(holderExpr(holder)); + if (UNEXPECTED(EG(exception))) { + return false; + } + if (containsSuperGlobal) { + continue; + } + tableAddNew(merged.table(), key, idx, createMaybeHolder(holder)); + } + } + + for (auto entry : theirs) { + zend_string *key = entry.stringKeyOrNull(); + zend_ulong idx = entry.indexKey(); + + if (pt_ht_exists(merged.table(), key, idx)) { + continue; + } + zv::Ref holder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return false; + } + bool containsSuperGlobal = pt_expr_contains_superglobal(holderExpr(holder)); + if (UNEXPECTED(EG(exception))) { + return false; + } + if (containsSuperGlobal) { + continue; + } + tableAddNew(merged.table(), key, idx, createMaybeHolder(holder)); + } + + return true; + } + + /* finishMerge()'s $filter closure for one holder */ + static bool filterKeepsHolder(zend_object *holder, bool *keep) + { + if (pt_holder_certainty_value(holder) == PT_TRI_YES) { + *keep = true; + return true; + } + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *funcCallCe = pt_class(PT_CLASS_FUNC_CALL); + zend_class_entry *virtualNodeCe = pt_class(PT_CLASS_VIRTUAL_NODE); + if (UNEXPECTED(variableCe == NULL || funcCallCe == NULL || virtualNodeCe == NULL)) { + return false; + } + zend_class_entry *exprCe = zv::ObjRef(holder).propAt(PT_ETH_PROP_EXPR).asObject()->ce; + *keep = instanceof_function(exprCe, variableCe) + || instanceof_function(exprCe, funcCallCe) + || instanceof_function(exprCe, virtualNodeCe); + return true; + } + + /* array_filter($holders, $filter) of finishMerge() */ + static bool filterHolders(zv::TableRef input, zv::Arr &filtered) + { + filtered = zv::Arr::create(input.size()); + for (auto entry : input) { + zv::Ref holder = entry.value().deref(); + if (UNEXPECTED(!pt_check_holder(holder.raw()))) { + return false; + } + bool keep; + if (UNEXPECTED(!filterKeepsHolder(holder.asObject(), &keep))) { + return false; + } + if (keep) { + tableAddNewCopy(filtered.table(), entry.stringKeyOrNull(), entry.indexKey(), holder); + } + } + return true; + } + + /* Calls a (possibly private) method on the object with the given args. */ + static bool callObjectMethod(zval *obj, const char *lcname, size_t len, uint32_t argc, zval *argv, zval *retval) + { + zend_class_entry *ce = Z_OBJCE_P(obj); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", ZSTR_VAL(ce->name), lcname); + return false; + } + zend_call_known_function(fn, Z_OBJ_P(obj), ce, retval, argc, argv, NULL); + return !EG(exception); + } + + /* $type->isSuperTypeOf($otherType)->result->value */ + static bool isSuperTypeOfValue(zval *type, zval *otherType, zend_long *out) + { + zval arg, retval; + ZVAL_COPY_VALUE(&arg, otherType); + if (UNEXPECTED(!callObjectMethod(type, "issupertypeof", sizeof("issupertypeof") - 1, 1, &arg, &retval))) { + return false; + } + zv::Val result = zv::Val::adopt(retval); + if (UNEXPECTED(!result.ref().isObject())) { + zend_throw_error(NULL, "phpstan_turbo: isSuperTypeOf did not return an object"); + return false; + } + int32_t resultOffset = pt_instance_prop_offset(Z_OBJCE_P(result.raw()), "result", sizeof("result") - 1); + if (UNEXPECTED(resultOffset < 0)) { + zend_throw_error(NULL, "phpstan_turbo: IsSuperTypeOfResult::result not found"); + return false; + } + zv::Ref resultProp = zv::ObjRef(result.ref().asObject()).propAtOffset((uint32_t) resultOffset).deref(); + if (UNEXPECTED(!resultProp.instanceOf(pt_ce_trinary))) { + zend_throw_error(NULL, "phpstan_turbo: IsSuperTypeOfResult::result is not a TrinaryLogic"); + return false; + } + *out = pt_trinary_value(resultProp.asObject()); + return true; + } + + /* $type->isConstantArray()->yes() */ + static bool isConstantArrayYes(zval *type, bool *out) + { + zval retval; + if (UNEXPECTED(!callObjectMethod(type, "isconstantarray", sizeof("isconstantarray") - 1, 0, NULL, &retval))) { + return false; + } + zv::Val result = zv::Val::adopt(retval); + if (UNEXPECTED(!result.ref().instanceOf(pt_ce_trinary))) { + zend_throw_error(NULL, "phpstan_turbo: isConstantArray did not return a TrinaryLogic"); + return false; + } + *out = pt_trinary_value(result.ref().asObject()) == PT_TRI_YES; + return true; + } + + /* new ExpressionTypeHolder($expr, new ErrorType(), TrinaryLogic::createNo()) */ + static zv::Val createNoErrorHolder(zval *exprSlot) + { + zend_class_entry *errorTypeCe = pt_class(PT_CLASS_ERROR_TYPE); + if (UNEXPECTED(errorTypeCe == NULL)) { + return zv::Val(); + } + zval errorTypeRaw; + object_init_ex(&errorTypeRaw, errorTypeCe); + zv::Val errorType = zv::Val::adopt(errorTypeRaw); + if (errorTypeCe->constructor != NULL) { + zend_call_known_instance_method(errorTypeCe->constructor, errorType.ref().asObject(), NULL, 0, NULL); + if (UNEXPECTED(EG(exception))) { + return zv::Val(); + } + } + /* pt_holder_create copies the type; the local ErrorType ref is released */ + zval holder; + pt_holder_create(&holder, exprSlot, errorType.raw(), PT_TRI_NO); + return zv::Val::adopt(holder); + } + + /* + * Appends new ConditionalExpressionHolder([$guardKey => $guardHolder], $typeHolder) + * under $result[$exprKey][getKey()]. Duplicates the input array into + * $result lazily on the first append. + */ + static bool appendConditional(zv::Arr &result, zv::TableRef input, zend_string *skey, zend_ulong idx, zend_string *guardKey, zend_ulong guardIdx, zv::Ref guardHolder, zv::Ref typeHolder) + { + zv::Arr conditions = zv::Arr::create(1); + tableAddNewCopy(conditions.table(), guardKey, guardIdx, guardHolder); + + zv::Str cehKey = zv::Str::adopt(pt_ceh_key_build(conditions.table(), typeHolder.raw())); + if (UNEXPECTED(cehKey.isNull())) { + return false; + } + + zval cehRaw; + object_init_ex(&cehRaw, pt_impl_class(PT_CLASS_CEH_IMPL, pt_ce_cond_expr_holder)); + zv::Val ceh = zv::Val::adopt(cehRaw); + zv::ObjRef cehObj(ceh.ref().asObject()); + cehObj.propAtWrite(PT_CEH_PROP_CONDS, std::move(conditions)); + cehObj.propAtWrite(PT_CEH_PROP_TYPEHOLDER, zv::Val::copyOf(typeHolder)); + + if (result.isUndef()) { + result = zv::Arr::adoptTable(zend_array_dup(input.table())); + } + + zval *inner = pt_ht_find(result.table(), skey, idx); + if (inner == NULL) { + zval newInner; + array_init(&newInner); + pt_ht_add_new(result.table(), skey, idx, &newInner); + inner = pt_ht_find(result.table(), skey, idx); + } else { + ZVAL_DEREF(inner); + if (UNEXPECTED(Z_TYPE_P(inner) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: conditional expressions entry is not an array"); + return false; + } + SEPARATE_ARRAY(inner); + } + + zval cehOut = ceh.take(); + zend_hash_update(Z_ARRVAL_P(inner), cehKey.get(), &cehOut); + return true; + } + + static bool strContains(zend_string *haystack, const char *needle, size_t len) + { + return zend_memnstr(ZSTR_VAL(haystack), needle, len, ZSTR_VAL(haystack) + ZSTR_LEN(haystack)) != NULL; + } + + static bool strContainsStr(zend_string *haystack, zend_string *needle) + { + return ZSTR_LEN(needle) <= ZSTR_LEN(haystack) + && zend_memnstr(ZSTR_VAL(haystack), ZSTR_VAL(needle), ZSTR_LEN(needle), ZSTR_VAL(haystack) + ZSTR_LEN(haystack)) != NULL; + } + + /* getIntertwinedRefRootVariableName()'s walk; returns a borrowed string */ + static zend_string *intertwinedRootVariableName(zend_object *expr) + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *arrayDimFetchCe = pt_class(PT_CLASS_ARRAY_DIM_FETCH); + + if (UNEXPECTED(variableCe == NULL || arrayDimFetchCe == NULL)) { + return NULL; + } + + for (;;) { + if (instanceof_function(expr->ce, variableCe)) { + pt_node_class_info *info = pt_get_node_class_info(expr->ce); + if (info == NULL || info->name_offset < 0) { + return NULL; + } + zv::Ref name = zv::ObjRef(expr).propAtOffset((uint32_t) info->name_offset).deref(); + return name.isString() ? name.asString() : NULL; /* borrowed */ + } + if (instanceof_function(expr->ce, arrayDimFetchCe)) { + int32_t varOffset = pt_instance_prop_offset(expr->ce, "var", sizeof("var") - 1); + if (varOffset < 0) { + return NULL; + } + zv::Ref var = zv::ObjRef(expr).propAtOffset((uint32_t) varOffset).deref(); + if (!var.isObject()) { + return NULL; + } + expr = var.asObject(); + continue; + } + return NULL; + } + } + + /* shouldInvalidate()'s per-node callback for pt_find_first_recursive() */ + static bool invalidationMatcher(zend_object *node, void *rawCtx) + { + pt_find_ctx *ctx = (pt_find_ctx *) rawCtx; + + if (ctx->is_this) { + zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); + if (UNEXPECTED(nameCe == NULL)) { + ctx->failed = true; + return false; + } + if (instanceof_function(node->ce, nameCe)) { + /* toLowerString() in [self, static, parent]? */ + zend_function *toLower = (zend_function *) zend_hash_str_find_ptr(&node->ce->function_table, "tolowerstring", sizeof("tolowerstring") - 1); + if (UNEXPECTED(toLower == NULL)) { + ctx->failed = true; + return false; + } + zval lowerRaw; + zend_call_known_function(toLower, node, node->ce, &lowerRaw, 0, NULL, NULL); + if (UNEXPECTED(EG(exception))) { + ctx->failed = true; + return false; + } + { + zv::Val lower = zv::Val::adopt(lowerRaw); + if (lower.ref().stringEquals("self") + || lower.ref().stringEquals("static") + || lower.ref().stringEquals("parent")) { + return true; + } + } + + /* getClassReflection() !== null && getClassReflection()->is(resolveName($node)) */ + if (!ctx->class_reflection_fetched) { + zend_class_entry *scopeCe = Z_OBJCE_P(ctx->scope); + zend_function *getClassReflection = (zend_function *) zend_hash_str_find_ptr(&scopeCe->function_table, "getclassreflection", sizeof("getclassreflection") - 1); + if (UNEXPECTED(getClassReflection == NULL)) { + ctx->failed = true; + return false; + } + zend_call_known_function(getClassReflection, Z_OBJ_P(ctx->scope), scopeCe, &ctx->class_reflection, 0, NULL, NULL); + if (UNEXPECTED(EG(exception))) { + ctx->failed = true; + return false; + } + ctx->class_reflection_fetched = true; + } + if (Z_TYPE(ctx->class_reflection) == IS_OBJECT) { + zend_class_entry *scopeCe = Z_OBJCE_P(ctx->scope); + zend_function *resolveName = (zend_function *) zend_hash_str_find_ptr(&scopeCe->function_table, "resolvename", sizeof("resolvename") - 1); + if (UNEXPECTED(resolveName == NULL)) { + ctx->failed = true; + return false; + } + zval resolvedRaw, nodeZv; + ZVAL_OBJ(&nodeZv, node); + zend_call_known_function(resolveName, Z_OBJ_P(ctx->scope), scopeCe, &resolvedRaw, 1, &nodeZv, NULL); + if (UNEXPECTED(EG(exception))) { + ctx->failed = true; + return false; + } + zv::Val resolved = zv::Val::adopt(resolvedRaw); + + zend_class_entry *reflectionCe = Z_OBJCE(ctx->class_reflection); + zend_function *isFn = (zend_function *) zend_hash_str_find_ptr(&reflectionCe->function_table, "is", sizeof("is") - 1); + if (UNEXPECTED(isFn == NULL)) { + ctx->failed = true; + return false; + } + zval isRetRaw; + zend_call_known_function(isFn, Z_OBJ(ctx->class_reflection), reflectionCe, &isRetRaw, 1, resolved.raw(), NULL); + if (UNEXPECTED(EG(exception))) { + ctx->failed = true; + return false; + } + zv::Val isRet = zv::Val::adopt(isRetRaw); + if (zend_is_true(isRet.raw())) { + return true; + } + } + } + } + + if (!instanceof_function(node->ce, ctx->target_ce)) { + return false; + } + + zv::Str nodeKey = zv::Str::adopt(pt_node_key(node, ctx->expr_printer)); + if (UNEXPECTED(nodeKey.isNull())) { + ctx->failed = true; + return false; + } + return zend_string_equals(nodeKey.get(), ctx->invalidate_str); + } + + /* + * The core of shouldInvalidateExpression(); $requireMoreCharacters is + * per-call (the conditional-holder scan passes false). Returns false and + * sets *failed on exception. + */ + static bool shouldInvalidate(const InvalidationQuery &query, zend_string *exprString, zend_object *expr, bool requireMoreCharacters, bool *failed) + { + zend_class_entry *variableCe = pt_class(PT_CLASS_VARIABLE); + zend_class_entry *intertwinedCe = pt_class(PT_CLASS_INTERTWINED_VAR); + + if (UNEXPECTED(variableCe == NULL || intertwinedCe == NULL)) { + *failed = true; + return false; + } + + /* Intertwined by-reference variables are never invalidated by their root variable */ + if (instanceof_function(expr->ce, intertwinedCe)) { + zend_class_entry *invalidateCe = Z_OBJCE_P(query.expressionToInvalidate); + if (instanceof_function(invalidateCe, variableCe)) { + pt_node_class_info *invalidateInfo = pt_get_node_class_info(invalidateCe); + if (invalidateInfo != NULL && invalidateInfo->name_offset >= 0) { + zv::Ref invalidateName = zv::ObjRef(query.expressionToInvalidate).propAtOffset((uint32_t) invalidateInfo->name_offset).deref(); + if (invalidateName.isString()) { + zend_string *name = invalidateName.asString(); + /* $expr->getVariableName() === name */ + int32_t variableNameOffset = pt_instance_prop_offset(expr->ce, "variableName", sizeof("variableName") - 1); + int32_t exprOffset = pt_instance_prop_offset(expr->ce, "expr", sizeof("expr") - 1); + int32_t assignedExprOffset = pt_instance_prop_offset(expr->ce, "assignedExpr", sizeof("assignedExpr") - 1); + if (variableNameOffset >= 0) { + zv::Ref variableName = zv::ObjRef(expr).propAtOffset((uint32_t) variableNameOffset).deref(); + if (variableName.isString() && zend_string_equals(variableName.asString(), name)) { + return false; + } + } + if (exprOffset >= 0) { + zv::Ref innerExpr = zv::ObjRef(expr).propAtOffset((uint32_t) exprOffset).deref(); + if (innerExpr.isObject()) { + zend_string *root = intertwinedRootVariableName(innerExpr.asObject()); + if (root != NULL && zend_string_equals(root, name)) { + return false; + } + } + } + if (assignedExprOffset >= 0) { + zv::Ref assignedExpr = zv::ObjRef(expr).propAtOffset((uint32_t) assignedExprOffset).deref(); + if (assignedExpr.isObject()) { + zend_string *root = intertwinedRootVariableName(assignedExpr.asObject()); + if (root != NULL && zend_string_equals(root, name)) { + return false; + } + } + } + } + } + } + } + + if (requireMoreCharacters && zend_string_equals(query.exprStringToInvalidate, exprString)) { + return false; + } + + /* Variables will not contain traversable expressions: direct compare */ + { + pt_node_class_info *info = pt_get_node_class_info(expr->ce); + if (info != NULL && info->is_variable && info->name_offset >= 0 && !requireMoreCharacters) { + zv::Ref name = zv::ObjRef(expr).propAtOffset((uint32_t) info->name_offset).deref(); + if (name.isString()) { + return zend_string_equals(query.exprStringToInvalidate, exprString); + } + } + } + + /* Compositional-key substring gate */ + if (!query.isThis + && !strContains(query.exprStringToInvalidate, "__phpstan", sizeof("__phpstan") - 1) + && !strContains(query.exprStringToInvalidate, "/*", 2) + && !strContains(exprString, "__phpstan", sizeof("__phpstan") - 1) + && !strContainsStr(exprString, query.exprStringToInvalidate)) { + return false; + } + + /* AST walk */ + { + pt_find_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.target_ce = Z_OBJCE_P(query.expressionToInvalidate); + ctx.invalidate_str = query.exprStringToInvalidate; + ctx.expr_printer = query.exprPrinter; + ctx.is_this = query.isThis; + ctx.scope = query.scope; + ZVAL_UNDEF(&ctx.class_reflection); + ctx.class_reflection_fetched = false; + ctx.failed = false; + + zend_object *found = pt_find_first_recursive(expr, invalidationMatcher, &ctx); + if (ctx.class_reflection_fetched) { + zval_ptr_dtor(&ctx.class_reflection); + } + if (UNEXPECTED(ctx.failed)) { + *failed = true; + return false; + } + if (found == NULL) { + return false; + } + } + + /* Post-checks calling back into the scope (rare paths) */ + if (requireMoreCharacters) { + zend_class_entry *propertyFetchCe = pt_class(PT_CLASS_PROPERTY_FETCH); + if (UNEXPECTED(propertyFetchCe == NULL)) { + *failed = true; + return false; + } + if (instanceof_function(expr->ce, propertyFetchCe)) { + zval argv[2]; + bool isReadonly; + ZVAL_OBJ(&argv[0], expr); + ZVAL_FALSE(&argv[1]); + if (UNEXPECTED(!pt_call_scope_bool(query.scope, "isreadonlypropertyfetch", sizeof("isreadonlypropertyfetch") - 1, 2, argv, &isReadonly))) { + *failed = true; + return false; + } + if (isReadonly) { + return false; + } + } + + if (query.invalidatingClass != NULL && Z_TYPE_P(query.invalidatingClass) == IS_OBJECT) { + zval argv[2]; + bool isPrivateOfOtherClass; + ZVAL_OBJ(&argv[0], expr); + ZVAL_COPY_VALUE(&argv[1], query.invalidatingClass); + if (UNEXPECTED(!pt_call_scope_bool(query.scope, "isprivatepropertyofdifferentclass", sizeof("isprivatepropertyofdifferentclass") - 1, 2, argv, &isPrivateOfOtherClass))) { + *failed = true; + return false; + } + if (isPrivateOfOtherClass) { + return false; + } + } + } + + return true; + } + + /* Collects one sureTypes/sureNotTypes table into the sortable working set. */ + static bool collectTypeSpecifications(zv::TableRef input, bool sure, TypeSpec *specs, uint32_t *count) + { + zend_class_entry *scalarCe = pt_class(PT_CLASS_SCALAR); + zend_class_entry *arrayExprCe = pt_class(PT_CLASS_ARRAY_EXPR); + zend_class_entry *unaryMinusCe = pt_class(PT_CLASS_UNARY_MINUS); + + if (UNEXPECTED(scalarCe == NULL || arrayExprCe == NULL || unaryMinusCe == NULL)) { + return false; + } + + for (auto entry : input) { + zv::Ref pair = entry.value().deref(); + if (UNEXPECTED(!pair.isArray())) { + zend_throw_error(NULL, "phpstan_turbo: sure type entry is not an array"); + return false; + } + zval *exprSlot = zend_hash_index_find(pair.asArrayTable(), 0); + zval *typeSlot = zend_hash_index_find(pair.asArrayTable(), 1); + if (UNEXPECTED(exprSlot == NULL || typeSlot == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: malformed sure type entry"); + return false; + } + zv::Ref expr = zv::Ref(exprSlot).deref(); + zv::Ref type = zv::Ref(typeSlot).deref(); + if (UNEXPECTED(!expr.isObject() || !type.isObject())) { + zend_throw_error(NULL, "phpstan_turbo: malformed sure type entry"); + return false; + } + + zend_class_entry *exprCe = expr.asObject()->ce; + if (instanceof_function(exprCe, scalarCe) || instanceof_function(exprCe, arrayExprCe)) { + continue; + } + if (instanceof_function(exprCe, unaryMinusCe)) { + int32_t subOffset = pt_instance_prop_offset(exprCe, "expr", sizeof("expr") - 1); + if (subOffset >= 0) { + zv::Ref sub = zv::ObjRef(expr.asObject()).propAtOffset((uint32_t) subOffset).deref(); + if (sub.instanceOf(scalarCe)) { + continue; + } + } + } + + TypeSpec *spec = &specs[*count]; + zend_string *key = entry.stringKeyOrNull(); + spec->exprString = key != NULL ? zend_string_copy(key) : zend_long_to_str((zend_long) entry.indexKey()); + ZVAL_COPY_VALUE(&spec->expr, expr.raw()); + ZVAL_COPY_VALUE(&spec->type, type.raw()); + spec->sure = sure; + spec->seq = *count; + (*count)++; + } + + return true; + } + + /* buildTypeSpecifications()'s usort comparator (stable via seq) */ + static int compareTypeSpecifications(const void *a, const void *b) + { + const TypeSpec *specA = (const TypeSpec *) a; + const TypeSpec *specB = (const TypeSpec *) b; + size_t lengthA = ZSTR_LEN(specA->exprString); + size_t lengthB = ZSTR_LEN(specB->exprString); + + if (lengthA != lengthB) { + return lengthA < lengthB ? -1 : 1; + } + if (specA->sure != specB->sure) { + return specB->sure - specA->sure; /* sure=true first */ + } + return specA->seq < specB->seq ? -1 : (specA->seq > specB->seq ? 1 : 0); + } + + /* + * matchConditionalExpressions()' shared tail of both passes: + * $conditions[$exprString][] = $conditionalExpression and + * $specifiedExpressions[$exprString] = its type holder. + */ + static void recordMatchedCondition(zv::Arr &conditions, zv::Arr &specified, zend_string *condKey, zend_ulong condIdx, zv::Ref conditionalExpression, zv::Ref typeHolder) + { + zval *group = pt_ht_find(conditions.table(), condKey, condIdx); + if (group == NULL) { + zval newGroup; + array_init(&newGroup); + pt_ht_add_new(conditions.table(), condKey, condIdx, &newGroup); + group = pt_ht_find(conditions.table(), condKey, condIdx); + } + Z_ADDREF_P(conditionalExpression.raw()); + add_next_index_zval(group, conditionalExpression.raw()); + Z_TRY_ADDREF_P(typeHolder.raw()); + pt_ht_update(specified.table(), condKey, condIdx, typeHolder.raw()); + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::ScopeOps; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +/* {{{ lifecycle (owns the scope-offsets cache) */ + +void pt_scope_ops_rinit() +{ + pt_scope_offsets_cache_inited = false; +} + +void pt_scope_ops_rshutdown() +{ + if (pt_scope_offsets_cache_inited) { + zend_hash_destroy(&pt_scope_offsets_cache); + pt_scope_offsets_cache_inited = false; + } +} + +/* }}} */ + +void pt_register_scope_ops() +{ + reg::Class cls("PHPStanTurbo\\ScopeOps"); + + cls.method("mergeVariableHolders", reg::PublicStatic, 2, { reg::arrayArg("ourVariableTypeHolders"), reg::arrayArg("theirVariableTypeHolders") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *ours, *theirs; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(ours) + Z_PARAM_ARRAY_HT(theirs) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::mergeVariableHolders(zv::TableRef(ours), zv::TableRef(theirs)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("finishMerge", reg::PublicStatic, 5, { reg::arrayArg("mergedExpressionTypes"), reg::arrayArg("ourExpressionTypes"), reg::arrayArg("theirExpressionTypes"), reg::arrayArg("ourNativeExpressionTypes"), reg::arrayArg("theirNativeExpressionTypes") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *merged, *ours_expr, *theirs_expr, *ours_native, *theirs_native; + ZEND_PARSE_PARAMETERS_START(5, 5) + Z_PARAM_ARRAY_HT(merged) + Z_PARAM_ARRAY_HT(ours_expr) + Z_PARAM_ARRAY_HT(theirs_expr) + Z_PARAM_ARRAY_HT(ours_native) + Z_PARAM_ARRAY_HT(theirs_native) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::finishMerge(zv::TableRef(merged), zv::TableRef(ours_expr), zv::TableRef(theirs_expr), zv::TableRef(ours_native), zv::TableRef(theirs_native)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("intersectConditionalExpressions", reg::PublicStatic, 2, { reg::arrayArg("ourConditionalExpressions"), reg::arrayArg("theirConditionalExpressions") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *ours, *theirs; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(ours) + Z_PARAM_ARRAY_HT(theirs) + ZEND_PARSE_PARAMETERS_END(); + ScopeOps::intersectConditionalExpressions(zv::TableRef(ours), zv::TableRef(theirs)).intoReturnValue(return_value); + }); + + cls.method("invalidateExpressionEntries", reg::PublicStatic, 9, { reg::objectArg("scope"), reg::objectArg("exprPrinter"), reg::stringArg("exprStringToInvalidate"), reg::objectArg("expressionToInvalidate"), reg::boolArg("requireMoreCharacters"), reg::objectArg("invalidatingClass", true), reg::arrayArg("expressionTypes"), reg::arrayArg("nativeExpressionTypes"), reg::arrayArg("conditionalExpressions") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *expr_printer, *expr_to_invalidate, *invalidating_class = NULL; + zend_string *invalidate_str; + bool require_more_characters; + HashTable *expression_types, *native_expression_types, *conditional_expressions; + ZEND_PARSE_PARAMETERS_START(9, 9) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(expr_printer) + Z_PARAM_STR(invalidate_str) + Z_PARAM_OBJECT(expr_to_invalidate) + Z_PARAM_BOOL(require_more_characters) + Z_PARAM_OBJECT_OR_NULL(invalidating_class) + Z_PARAM_ARRAY_HT(expression_types) + Z_PARAM_ARRAY_HT(native_expression_types) + Z_PARAM_ARRAY_HT(conditional_expressions) + ZEND_PARSE_PARAMETERS_END(); + pt_init_strs(); + zv::Val result = ScopeOps::invalidateExpressionEntries(scope, expr_printer, invalidate_str, expr_to_invalidate, require_more_characters, invalidating_class, zv::TableRef(expression_types), zv::TableRef(native_expression_types), zv::TableRef(conditional_expressions)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("shouldInvalidateExpression", reg::PublicStatic, 6, { reg::objectArg("scope"), reg::objectArg("exprPrinter"), reg::stringArg("exprStringToInvalidate"), reg::objectArg("exprToInvalidate"), reg::objectArg("expr"), reg::stringArg("exprString"), reg::boolArg("requireMoreCharacters"), reg::objectArg("invalidatingClass", true) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *expr_printer, *expr_to_invalidate, *expr, *invalidating_class = NULL; + zend_string *invalidate_str, *expr_string; + bool require_more_characters = false; + ZEND_PARSE_PARAMETERS_START(6, 8) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(expr_printer) + Z_PARAM_STR(invalidate_str) + Z_PARAM_OBJECT(expr_to_invalidate) + Z_PARAM_OBJECT(expr) + Z_PARAM_STR(expr_string) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(require_more_characters) + Z_PARAM_OBJECT_OR_NULL(invalidating_class) + ZEND_PARSE_PARAMETERS_END(); + pt_init_strs(); + bool failed = false; + bool result = ScopeOps::shouldInvalidateExpression(scope, expr_printer, invalidate_str, expr_to_invalidate, Z_OBJ_P(expr), expr_string, require_more_characters, invalidating_class, &failed); + if (UNEXPECTED(failed)) { + RETURN_THROWS(); + } + RETURN_BOOL(result); + }); + + cls.method("getIntertwinedRefRootVariableName", reg::PublicStatic, 1, { reg::objectArg("expr") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(expr) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::getIntertwinedRefRootVariableName(Z_OBJ_P(expr)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("buildTypeSpecifications", reg::PublicStatic, 2, { reg::arrayArg("sureTypes"), reg::arrayArg("sureNotTypes") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *sure_types, *sure_not_types; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(sure_types) + Z_PARAM_ARRAY_HT(sure_not_types) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::buildTypeSpecifications(zv::TableRef(sure_types), zv::TableRef(sure_not_types)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("matchConditionalExpressions", reg::PublicStatic, 2, { reg::arrayArg("conditionalExpressions"), reg::arrayArg("specifiedExpressions") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *conditional, *specified_input; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(conditional) + Z_PARAM_ARRAY_HT(specified_input) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::matchConditionalExpressions(zv::TableRef(conditional), zv::TableRef(specified_input)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("createConditionalExpressions", reg::PublicStatic, 4, { reg::arrayArg("conditionalExpressions"), reg::arrayArg("ourExpressionTypes"), reg::arrayArg("theirExpressionTypes"), reg::arrayArg("mergedExpressionTypes") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *conditional, *ours, *theirs, *merged; + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_ARRAY_HT(conditional) + Z_PARAM_ARRAY_HT(ours) + Z_PARAM_ARRAY_HT(theirs) + Z_PARAM_ARRAY_HT(merged) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::createConditionalExpressions(zv::TableRef(conditional), zv::TableRef(ours), zv::TableRef(theirs), zv::TableRef(merged)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("nodeKey", reg::PublicStatic, 2, { reg::objectArg("node"), reg::objectArg("exprPrinter") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node, *expr_printer; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(node) + Z_PARAM_OBJECT(expr_printer) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::nodeKey(Z_OBJ_P(node), expr_printer); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("getTypeFromCache", reg::PublicStatic, 3, { reg::objectArg("scope"), reg::objectArg("node"), reg::any("key", true) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *node, *key_out; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(node) + Z_PARAM_ZVAL(key_out) + ZEND_PARSE_PARAMETERS_END(); + zend_string *key = NULL; + zv::Val result = ScopeOps::getTypeFromCache(scope, Z_OBJ_P(node), &key); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + if (key != NULL) { + /* hand the computed key to the by-ref parameter (hit and miss) */ + if (Z_ISREF_P(key_out)) { + ZEND_TRY_ASSIGN_REF_STR(key_out, key); + } else { + zend_string_release(key); + } + } + result.intoReturnValue(return_value); + }); + + cls.method("hasVariableType", reg::PublicStatic, 2, { reg::objectArg("scope"), reg::stringArg("variableName") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + zend_string *variable_name; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(scope) + Z_PARAM_STR(variable_name) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::hasVariableType(scope, variable_name); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("scopeWith", reg::PublicStatic, 9, { reg::objectArg("scope"), reg::arrayArg("expressionTypes"), reg::arrayArg("nativeExpressionTypes"), reg::arrayArg("conditionalExpressions"), reg::arrayArg("currentlyAssignedExpressions"), reg::arrayArg("currentlyAllowedUndefinedExpressions"), reg::arrayArg("inFunctionCallsStack"), reg::boolArg("inFirstLevelStatement"), reg::boolArg("afterExtractCall") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope; + HashTable *expression_types, *native_expression_types, *conditional_expressions; + HashTable *currently_assigned, *currently_allowed_undefined, *in_function_calls_stack; + bool in_first_level_statement, after_extract_call; + ZEND_PARSE_PARAMETERS_START(9, 9) + Z_PARAM_OBJECT(scope) + Z_PARAM_ARRAY_HT(expression_types) + Z_PARAM_ARRAY_HT(native_expression_types) + Z_PARAM_ARRAY_HT(conditional_expressions) + Z_PARAM_ARRAY_HT(currently_assigned) + Z_PARAM_ARRAY_HT(currently_allowed_undefined) + Z_PARAM_ARRAY_HT(in_function_calls_stack) + Z_PARAM_BOOL(in_first_level_statement) + Z_PARAM_BOOL(after_extract_call) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::scopeWith(scope, expression_types, native_expression_types, conditional_expressions, currently_assigned, currently_allowed_undefined, in_function_calls_stack, in_first_level_statement, after_extract_call); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("invalidateMethodsOnExpression", reg::PublicStatic, 4, { reg::objectArg("exprPrinter"), reg::stringArg("exprStringToInvalidate"), reg::arrayArg("expressionTypes"), reg::arrayArg("nativeExpressionTypes") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr_printer; + zend_string *invalidate_str; + HashTable *expression_types, *native_expression_types; + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_OBJECT(expr_printer) + Z_PARAM_STR(invalidate_str) + Z_PARAM_ARRAY_HT(expression_types) + Z_PARAM_ARRAY_HT(native_expression_types) + ZEND_PARSE_PARAMETERS_END(); + pt_init_strs(); + zv::Val result = ScopeOps::invalidateMethodsOnExpression(expr_printer, invalidate_str, zv::TableRef(expression_types), zv::TableRef(native_expression_types)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("expressionTypeByKey", reg::PublicStatic, 3, { reg::objectArg("scope"), reg::objectArg("node"), reg::stringArg("exprString") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *node; + zend_string *expr_string; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(node) + Z_PARAM_STR(expr_string) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::expressionTypeByKey(scope, Z_OBJ_P(node), expr_string); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("hasExpressionType", reg::PublicStatic, 3, { reg::objectArg("scope"), reg::objectArg("node"), reg::objectArg("exprPrinter") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *node, *expr_printer; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(node) + Z_PARAM_OBJECT(expr_printer) + ZEND_PARSE_PARAMETERS_END(); + zv::Val result = ScopeOps::hasExpressionType(scope, Z_OBJ_P(node), expr_printer); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + pt_ce_scope_ops = cls.register_(); + (void) pt_ce_scope_ops; +} + +/* }}} */ diff --git a/turbo-ext/src/TrinaryLogic.cpp b/turbo-ext/src/TrinaryLogic.cpp new file mode 100644 index 00000000000..10511c54bda --- /dev/null +++ b/turbo-ext/src/TrinaryLogic.cpp @@ -0,0 +1,494 @@ +/* + * PHPStanTurbo\TrinaryLogic — native implementation of PHPStan\TrinaryLogic. + * + * When the extension is enabled, PHPStan\TrinaryLogic is declared as an empty + * final subclass of this class (turbo-ext/stubs/TrinaryLogic.php). Instances + * are always of that subclass — the singletons are created from the + * configured trinaryLogicImpl class — so userland type hints keep working. + * + * The logic lives in the TrinaryLogic handle class below, structured to + * mirror src/TrinaryLogic.php method for method; the PHP_METHOD functions at + * the bottom are only the engine ABI glue (parameter parsing + delegation). + */ + +#include "support.h" +#include "zv.h" + +namespace phpstanturbo { + +/* Mirrors PHPStan\TrinaryLogic. State lives in the PHP object's $value. */ +class TrinaryLogic +{ +public: + static constexpr zend_long YES = PT_TRI_YES; + static constexpr zend_long MAYBE = PT_TRI_MAYBE; + static constexpr zend_long NO = PT_TRI_NO; + + explicit TrinaryLogic(zend_object *self) : self(self) {} + + zend_long value() const { return pt_trinary_value(self); } + + static zv::Val create(zend_long value) { return zv::Val::copyOf(zv::Ref(pt_trinary_singleton(value))); } + static zv::Val createYes() { return create(YES); } + static zv::Val createNo() { return create(NO); } + static zv::Val createMaybe() { return create(MAYBE); } + static zv::Val createFromBoolean(bool value) { return create(value ? YES : NO); } + + bool yes() const { return value() == YES; } + bool maybe() const { return value() == MAYBE; } + bool no() const { return value() == NO; } + + /* and() — a C++ keyword, hence the underscore */ + zv::Val and_(const TrinaryLogic *operand, zval *rest, uint32_t restCount) const + { + zend_long acc = value(); + acc &= operand != NULL ? operand->value() : YES; + for (uint32_t i = 0; i < restCount; i++) { + acc &= TrinaryLogic(zv::Ref(&rest[i]).deref().asObject()).value(); + } + return create(acc); + } + + /* or() — a C++ keyword, hence the underscore */ + zv::Val or_(const TrinaryLogic *operand, zval *rest, uint32_t restCount) const + { + zend_long acc = value(); + acc |= operand != NULL ? operand->value() : NO; + for (uint32_t i = 0; i < restCount; i++) { + acc |= TrinaryLogic(zv::Ref(&rest[i]).deref().asObject()).value(); + } + return create(acc); + } + + static zv::Val extremeIdentity(zval *operands, uint32_t count) + { + zend_long min, max; + min = max = TrinaryLogic(zv::Ref(&operands[0]).deref().asObject()).value(); + for (uint32_t i = 1; i < count; i++) { + zend_long v = TrinaryLogic(zv::Ref(&operands[i]).deref().asObject()).value(); + if (v < min) { + min = v; + } + if (v > max) { + max = v; + } + } + return create(min == max ? min : MAYBE); + } + + static zv::Val maxMin(zval *operands, uint32_t count) + { + zend_long max = NO; + zend_long min = YES; + for (uint32_t i = 0; i < count; i++) { + zend_long v = TrinaryLogic(zv::Ref(&operands[i]).deref().asObject()).value(); + max |= v; + min &= v; + } + return create(max == YES ? YES : min); + } + + zv::Val negate() const { return create(3 >> value()); } + + bool equals(const TrinaryLogic &other) const { return self == other.self; } + + /* returns the greater operand's object, or null when equal */ + zv::Val compareTo(zval *thisZv, zval *otherZv) const + { + TrinaryLogic other(Z_OBJ_P(otherZv)); + if (value() > other.value()) { + return zv::Val::copyOf(zv::Ref(thisZv)); + } + if (other.value() > value()) { + return zv::Val::copyOf(zv::Ref(otherZv)); + } + return zv::Val::null(); + } + + const char *describe() const + { + if (value() == YES) { + return "Yes"; + } + if (value() == MAYBE) { + return "Maybe"; + } + return "No"; + } + + /* BooleanType for maybe, ConstantBooleanType(yes/no) otherwise; + * UNDEF result means a pending exception */ + zv::Val toBooleanType() const + { + if (maybe()) { + return constructConfigured(PT_CLASS_BOOLEAN_TYPE, NULL, 0); + } + zval arg; + ZVAL_BOOL(&arg, yes()); + return constructConfigured(PT_CLASS_CONSTANT_BOOLEAN_TYPE, &arg, 1); + } + +private: + zend_object *self; + + static zv::Val constructConfigured(int classIdx, zval *args, uint32_t argc) + { + zend_class_entry *ce = pt_class(classIdx); + if (UNEXPECTED(ce == NULL)) { + return zv::Val(); + } + zval obj; + if (UNEXPECTED(object_init_ex(&obj, ce) != SUCCESS)) { + return zv::Val(); + } + if (ce->constructor != NULL) { + zend_call_known_instance_method(ce->constructor, Z_OBJ(obj), NULL, argc, args); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&obj); + return zv::Val(); + } + } + return zv::Val::adopt(obj); + } +}; + +/* + * The lazy* trio shares one accumulation loop over callback results, + * mirroring the closures the PHP implementation passes around. + */ +class LazyEvaluation +{ +public: + enum Mode + { + AND, + OR, + MAX_MIN, + }; + + LazyEvaluation(zend_fcall_info fci, zend_fcall_info_cache fcc) : fci(fci), fcc(fcc) {} + + /* calls the callback for one item; UNDEF result means a pending exception */ + zv::Val callbackResult(zv::Ref item) + { + zval retval, param; + ZVAL_COPY_VALUE(¶m, item.raw()); + fci.retval = &retval; + fci.param_count = 1; + fci.params = ¶m; + fci.named_params = NULL; + + if (UNEXPECTED(zend_call_function(&fci, &fcc) != SUCCESS || EG(exception))) { + return zv::Val(); + } + if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || !instanceof_function(Z_OBJCE(retval), pt_ce_trinary))) { + zval_ptr_dtor(&retval); + zend_type_error("Return value of the callback must be of type %s", ZSTR_VAL(pt_ce_trinary->name)); + return zv::Val(); + } + return zv::Val::adopt(retval); + } + + /* the shared and/or/maxMin accumulation; UNDEF means a pending exception */ + zv::Val run(Mode mode, zval *thisZv, zv::ArrRef objects) + { + zend_long thisValue = 0; + if (mode != MAX_MIN) { + thisValue = TrinaryLogic(Z_OBJ_P(thisZv)).value(); + if (mode == AND && thisValue == TrinaryLogic::NO) { + return zv::Val::copyOf(zv::Ref(thisZv)); + } + if (mode == OR && thisValue == TrinaryLogic::YES) { + return zv::Val::copyOf(zv::Ref(thisZv)); + } + } + + zend_long acc = mode == OR ? TrinaryLogic::NO : TrinaryLogic::YES; + + for (auto entry : objects) { + zv::Val result = callbackResult(entry.value()); + if (result.isUndef()) { + return zv::Val(); + } + zend_long resultValue = TrinaryLogic(zv::Ref(result.raw()).asObject()).value(); + + if (mode == AND && resultValue == TrinaryLogic::NO) { + return result; + } + if ((mode == OR || mode == MAX_MIN) && resultValue == TrinaryLogic::YES) { + return result; + } + + if (mode == OR) { + acc |= resultValue; + } else { + acc &= resultValue; + } + } + + if (mode == AND) { + acc &= thisValue; + } else if (mode == OR) { + acc |= thisValue; + } + + return TrinaryLogic::create(acc); + } + + /* lazyExtremeIdentity: all results identical → that result, else maybe */ + zv::Val runExtremeIdentity(zv::ArrRef objects) + { + zv::Val last; + for (auto entry : objects) { + zv::Val result = callbackResult(entry.value()); + if (result.isUndef()) { + return zv::Val(); + } + if (last.isUndef()) { + last = std::move(result); + continue; + } + if (zv::Ref(result.raw()).asObject() != zv::Ref(last.raw()).asObject()) { + return TrinaryLogic::create(TrinaryLogic::MAYBE); + } + } + return last; + } + +private: + zend_fcall_info fci; + zend_fcall_info_cache fcc; +}; + +} // namespace phpstanturbo + +using phpstanturbo::LazyEvaluation; +using phpstanturbo::TrinaryLogic; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +#define TRINARY_CLASS "PHPStanTurbo\\TrinaryLogic" + +static zend_result pt_verify_trinary_variadic(zval *args, uint32_t count, uint32_t offset) +{ + for (uint32_t i = 0; i < count; i++) { + if (UNEXPECTED(!zv::Ref(&args[i]).deref().instanceOf(pt_ce_trinary))) { + zend_argument_type_error(offset + i, "must be of type %s", ZSTR_VAL(pt_ce_trinary->name)); + return FAILURE; + } + } + return SUCCESS; +} + +static void pt_trinary_lazy(INTERNAL_FUNCTION_PARAMETERS, LazyEvaluation::Mode mode) +{ + HashTable *objects; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(objects) + Z_PARAM_FUNC(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + /* no empty-array check for MAX_MIN: unlike extremeIdentity()/maxMin(), the + * PHP twin's lazyMaxMin([]) returns Yes ($min starts at YES), and run()'s + * accumulator reproduces that */ + + zval objectsZv; + ZVAL_ARR(&objectsZv, objects); + zv::Val result = LazyEvaluation(fci, fcc).run(mode, ZEND_THIS, zv::ArrRef(&objectsZv)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); +} + +static void pt_trinary_variadic_op(INTERNAL_FUNCTION_PARAMETERS, bool extremeIdentity) +{ + zval *operands = NULL; + uint32_t count = 0; + + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('+', operands, count) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(count == 0)) { + pt_throw_should_not_happen(); + RETURN_THROWS(); + } + if (UNEXPECTED(pt_verify_trinary_variadic(operands, count, 1) != SUCCESS)) { + RETURN_THROWS(); + } + + (extremeIdentity ? TrinaryLogic::extremeIdentity(operands, count) : TrinaryLogic::maxMin(operands, count)).intoReturnValue(return_value); +} + +static void pt_trinary_and_or(INTERNAL_FUNCTION_PARAMETERS, bool isAnd) +{ + zval *operand = NULL; + zval *rest = NULL; + uint32_t restCount = 0; + + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(operand, pt_ce_trinary) + Z_PARAM_VARIADIC('+', rest, restCount) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(pt_verify_trinary_variadic(rest, restCount, 2) != SUCCESS)) { + RETURN_THROWS(); + } + + TrinaryLogic self(Z_OBJ_P(ZEND_THIS)); + TrinaryLogic operandHandle(operand != NULL ? Z_OBJ_P(operand) : NULL); + const TrinaryLogic *operandPtr = operand != NULL ? &operandHandle : NULL; + (isAnd ? self.and_(operandPtr, rest, restCount) : self.or_(operandPtr, rest, restCount)).intoReturnValue(return_value); +} + +void pt_register_trinary_logic() +{ + reg::Class cls("PHPStanTurbo\\TrinaryLogic"); + /* not final: the stub subclass PHPStan\TrinaryLogic extends this class; + * "value" must stay the first declared property (OBJ_PROP_NUM slot 0) */ + cls.privateLongProperty("value", 0); + + cls.method("__construct", reg::Private, 1, { reg::longArg("value") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zend_long value; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(value) + ZEND_PARSE_PARAMETERS_END(); + ZVAL_LONG(OBJ_PROP_NUM(Z_OBJ_P(ZEND_THIS), PT_TRI_PROP_VALUE), value); + }); + + cls.method("createYes", reg::PublicStatic, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + TrinaryLogic::createYes().intoReturnValue(return_value); + }); + + cls.method("createNo", reg::PublicStatic, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + TrinaryLogic::createNo().intoReturnValue(return_value); + }); + + cls.method("createMaybe", reg::PublicStatic, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + TrinaryLogic::createMaybe().intoReturnValue(return_value); + }); + + cls.method("createFromBoolean", reg::PublicStatic, 1, { reg::boolArg("value") }, [](INTERNAL_FUNCTION_PARAMETERS) { + bool value; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_BOOL(value) + ZEND_PARSE_PARAMETERS_END(); + TrinaryLogic::createFromBoolean(value).intoReturnValue(return_value); + }); + + cls.method("yes", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(TrinaryLogic(Z_OBJ_P(ZEND_THIS)).yes()); + }); + + cls.method("maybe", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(TrinaryLogic(Z_OBJ_P(ZEND_THIS)).maybe()); + }); + + cls.method("no", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(TrinaryLogic(Z_OBJ_P(ZEND_THIS)).no()); + }); + + cls.method("toBooleanType", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + zv::Val result = TrinaryLogic(Z_OBJ_P(ZEND_THIS)).toBooleanType(); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("and", reg::Public, 0, { reg::obj("operand", TRINARY_CLASS, true), reg::variadicObj("rest", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_and_or(INTERNAL_FUNCTION_PARAM_PASSTHRU, true); + }); + + cls.method("lazyAnd", reg::Public, 2, { reg::arrayArg("objects"), reg::callableArg("callback") }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_lazy(INTERNAL_FUNCTION_PARAM_PASSTHRU, LazyEvaluation::AND); + }); + + cls.method("or", reg::Public, 0, { reg::obj("operand", TRINARY_CLASS, true), reg::variadicObj("rest", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_and_or(INTERNAL_FUNCTION_PARAM_PASSTHRU, false); + }); + + cls.method("lazyOr", reg::Public, 2, { reg::arrayArg("objects"), reg::callableArg("callback") }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_lazy(INTERNAL_FUNCTION_PARAM_PASSTHRU, LazyEvaluation::OR); + }); + + cls.method("extremeIdentity", reg::PublicStatic, 0, { reg::variadicObj("operands", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_variadic_op(INTERNAL_FUNCTION_PARAM_PASSTHRU, true); + }); + + cls.method("lazyExtremeIdentity", reg::PublicStatic, 2, { reg::arrayArg("objects"), reg::callableArg("callback") }, [](INTERNAL_FUNCTION_PARAMETERS) { + HashTable *objects; + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY_HT(objects) + Z_PARAM_FUNC(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(zend_hash_num_elements(objects) == 0)) { + pt_throw_should_not_happen(); + RETURN_THROWS(); + } + + zval objectsZv; + ZVAL_ARR(&objectsZv, objects); + zv::Val result = LazyEvaluation(fci, fcc).runExtremeIdentity(zv::ArrRef(&objectsZv)); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("maxMin", reg::PublicStatic, 0, { reg::variadicObj("operands", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_variadic_op(INTERNAL_FUNCTION_PARAM_PASSTHRU, false); + }); + + cls.method("lazyMaxMin", reg::PublicStatic, 2, { reg::arrayArg("objects"), reg::callableArg("callback") }, [](INTERNAL_FUNCTION_PARAMETERS) { + pt_trinary_lazy(INTERNAL_FUNCTION_PARAM_PASSTHRU, LazyEvaluation::MAX_MIN); + }); + + cls.method("negate", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + TrinaryLogic(Z_OBJ_P(ZEND_THIS)).negate().intoReturnValue(return_value); + }); + + cls.method("equals", reg::Public, 1, { reg::obj("other", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_trinary) + ZEND_PARSE_PARAMETERS_END(); + RETURN_BOOL(TrinaryLogic(Z_OBJ_P(ZEND_THIS)).equals(TrinaryLogic(Z_OBJ_P(other)))); + }); + + cls.method("compareTo", reg::Public, 1, { reg::obj("other", TRINARY_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *other; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, pt_ce_trinary) + ZEND_PARSE_PARAMETERS_END(); + TrinaryLogic(Z_OBJ_P(ZEND_THIS)).compareTo(ZEND_THIS, other).intoReturnValue(return_value); + }); + + cls.method("describe", reg::Public, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_STRING(TrinaryLogic(Z_OBJ_P(ZEND_THIS)).describe()); + }); + + pt_ce_trinary = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/TypeCombinatorCache.cpp b/turbo-ext/src/TypeCombinatorCache.cpp new file mode 100644 index 00000000000..70498bd7547 --- /dev/null +++ b/turbo-ext/src/TypeCombinatorCache.cpp @@ -0,0 +1,593 @@ +/* + * PHPStanTurbo\TypeCombinatorCache — native implementation of + * PHPStan\Type\TypeCombinatorCache. + * + * TypeCombinator::union()/intersect()/remove() route through this class when the + * extension is active. Roughly 91% of the calls in an analysis run repeat an + * argument tuple whose result was already computed, so each operation is memoized + * on a structural key of its arguments; a miss calls back into the PHP twin + * (TypeCombinator::doUnion() and friends), which stays the reference implementation. + * + * Two structures back this: + * + * - a per-object 128-bit structural hash of every Type, cached in a *weak* map so + * the entry disappears with the object: the cache retains nothing and an object + * address can never be mistaken for a freed one's. Composite types hash from + * their children's cached hashes, so hashing a new type costs O(#properties), + * not O(tree). + * - the memo itself, keyed by the operation plus its arguments' hashes. + * + * The hash is 128 bits precisely so the key can be trusted without keeping the + * arguments alive to re-verify a hit: over the ~10^5 distinct keys of a run the + * collision probability is ~10^-27. A 64-bit key would be ~10^-9 per run, which + * across a user base is a silently wrong analysis result — not acceptable. + * + * The memo is cleared whenever a container is created (TypeCombinator::clearCache()). + * Memoization hands back *shared* Type instances, and a Type lazily resolves a + * ClassReflection belonging to the container that created it — so entries must not + * outlive their container. Production runs one container per process; the test + * suite does not. + */ + +#include "support.h" +#include "zv.h" + +#include + +namespace phpstanturbo { + +/* Beyond this argument count a call is computed without consulting the memo: + * the key would not fit the stack buffer and such calls are vanishingly rare. */ +static constexpr uint32_t MEMO_ARGS_LIMIT = 16; + +/* Depth guard for the structural walk. Types nest ~10 deep; anything beyond this + * is not hashed at all (the call bypasses the memo) rather than hashed coarsely, + * because a coarse hash would be an unsound key. */ +static constexpr uint32_t HASH_DEPTH_LIMIT = 64; + +/* Safety net on memo growth; a self-analysis run settles at ~1.3e5 entries. */ +static constexpr uint32_t MEMO_ENTRIES_LIMIT = 1 << 19; + +struct Hash128 +{ + uint64_t a; + uint64_t b; +}; + +/* A memo entry owns its result. */ +struct MemoEntry +{ + zend_object *result; +}; + +/* The weak map derives its key from the object address the way the engine does + * (see zend_weakrefs.c). That derivation is an engine internal, so the entry keeps + * the object it belongs to and every hit is checked against it: should the engine + * ever key differently, this degrades to a cache miss instead of handing back + * another object's hash. */ +struct TypeHash +{ + zend_object *obj; + Hash128 hash; +}; + +/* Objects outside the type system (ClassReflection, method reflections held in + * ObjectType::$methodCache, …) take part in the hash by identity. They must NOT be hashed + * by address: addresses are reused once an object is freed, so two different types could + * hash alike — and they vary between runs, which made results non-deterministic. Each such + * object instead gets a serial that is never reused. */ +struct ObjSerial +{ + zend_object *obj; + uint64_t serial; +}; + +static inline zend_ulong weakKey(const zend_object *obj) +{ + return ((zend_ulong) (uintptr_t) obj) >> ZEND_MM_ALIGNMENT_LOG2; +} + +static HashTable pt_type_hashes; /* weak: zend_object* -> Hash128* */ +static HashTable pt_memo; /* binary key -> MemoEntry* */ +static HashTable pt_ce_kinds; /* zend_class_entry* -> kind|slots */ +static HashTable pt_obj_serials; /* weak: zend_object* -> ObjSerial* (identity-hashed objects) */ +static uint64_t pt_next_serial = 1; +static bool pt_cache_inited = false; + + +static zend_class_entry *pt_guard_ce = NULL; +static uint32_t pt_guard_offset = 0; +static bool pt_guard_resolved = false; +static bool pt_guard_unavailable = false; + +static zend_function *pt_fn_do_union = NULL; +static zend_function *pt_fn_do_intersect = NULL; +static zend_function *pt_fn_do_remove = NULL; + +/* {{{ 128-bit FNV-1a, two independent accumulators fed by one walk */ + +static constexpr uint64_t FNV_OFFSET_A = 0xcbf29ce484222325ULL; +static constexpr uint64_t FNV_PRIME_A = 0x100000001b3ULL; +static constexpr uint64_t FNV_OFFSET_B = 0x9e3779b97f4a7c15ULL; +static constexpr uint64_t FNV_PRIME_B = 0xff51afd7ed558ccdULL; + +static inline void mixByte(Hash128 &h, uint8_t byte) +{ + h.a = (h.a ^ byte) * FNV_PRIME_A; + h.b = (h.b ^ byte) * FNV_PRIME_B; +} + +static inline void mixBytes(Hash128 &h, const void *data, size_t len) +{ + const uint8_t *p = (const uint8_t *) data; + for (size_t i = 0; i < len; i++) { + mixByte(h, p[i]); + } +} + +static inline void mixU64(Hash128 &h, uint64_t value) +{ + mixBytes(h, &value, sizeof(value)); +} + +/* }}} */ + +/* {{{ per-class-entry plan: is this object hashed structurally, and how many slots */ + +enum CeKind : uint8_t { + CE_IDENTITY = 0, /* not a type-system value object: hashed by address */ + CE_STRUCTURAL = 1, +}; + +struct CePlan +{ + CeKind kind; + uint32_t slots; +}; + +static bool ceNameHasPrefix(const zend_class_entry *ce, const char *prefix, size_t len) +{ + return ZSTR_LEN(ce->name) >= len && memcmp(ZSTR_VAL(ce->name), prefix, len) == 0; +} + +static CePlan cePlan(zend_class_entry *ce) +{ + zval *cached = zend_hash_index_find(&pt_ce_kinds, (zend_ulong) (uintptr_t) ce); + if (cached != NULL) { + zend_long packed = Z_LVAL_P(cached); + return { (CeKind) (packed & 1), (uint32_t) (packed >> 1) }; + } + + zend_class_entry *typeCe = pt_class(PT_CLASS_TYPE); + CeKind kind = CE_IDENTITY; + if ((typeCe != NULL && instanceof_function(ce, typeCe)) + || (pt_ce_trinary != NULL && instanceof_function(ce, pt_ce_trinary)) + || ceNameHasPrefix(ce, "PHPStan\\Type\\", sizeof("PHPStan\\Type\\") - 1) + || ceNameHasPrefix(ce, "PHPStan\\Php\\", sizeof("PHPStan\\Php\\") - 1)) { + kind = CE_STRUCTURAL; + } + + CePlan plan = { kind, (uint32_t) ce->default_properties_count }; + zval packed; + ZVAL_LONG(&packed, ((zend_long) plan.slots << 1) | (zend_long) plan.kind); + zend_hash_index_add(&pt_ce_kinds, (zend_ulong) (uintptr_t) ce, &packed); + + return plan; +} + +/* }}} */ + +/* {{{ structural hashing */ + +static uint64_t objSerial(zend_object *obj) +{ + ObjSerial *known = (ObjSerial *) zend_hash_index_find_ptr(&pt_obj_serials, weakKey(obj)); + if (known != NULL && known->obj == obj) { + return known->serial; + } + + ObjSerial *entry = (ObjSerial *) emalloc(sizeof(ObjSerial)); + entry->obj = obj; + entry->serial = pt_next_serial++; + if (zend_weakrefs_hash_add_ptr(&pt_obj_serials, obj, entry) == NULL) { + uint64_t serial = entry->serial; + efree(entry); + return serial; + } + + return entry->serial; +} + +static bool hashObject(zend_object *obj, Hash128 &out, uint32_t depth); + +static bool hashZval(zval *value, Hash128 &h, uint32_t depth) +{ + ZVAL_DEREF(value); + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + /* An uninitialized typed property is NOT null — ConstantArrayType::$unsealed + * distinguishes the two, and conflating them merges sealed with unsealed. */ + mixByte(h, 1); + return true; + case IS_NULL: + mixByte(h, 2); + return true; + case IS_FALSE: + mixByte(h, 3); + return true; + case IS_TRUE: + mixByte(h, 4); + return true; + case IS_LONG: + mixByte(h, 5); + mixU64(h, (uint64_t) Z_LVAL_P(value)); + return true; + case IS_DOUBLE: { + double d = Z_DVAL_P(value); + uint64_t bits; + memcpy(&bits, &d, sizeof(bits)); + mixByte(h, 6); + mixU64(h, bits); + return true; + } + case IS_STRING: { + zend_string *str = Z_STR_P(value); + mixByte(h, 7); + mixU64(h, (uint64_t) ZSTR_LEN(str)); + mixBytes(h, ZSTR_VAL(str), ZSTR_LEN(str)); + return true; + } + case IS_ARRAY: { + zv::ArrRef arr(value); + mixByte(h, 8); + mixU64(h, (uint64_t) zend_hash_num_elements(arr.table())); + for (auto entry : arr) { + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + mixByte(h, 9); + mixU64(h, (uint64_t) ZSTR_LEN(key)); + mixBytes(h, ZSTR_VAL(key), ZSTR_LEN(key)); + } else { + mixByte(h, 10); + mixU64(h, (uint64_t) entry.indexKey()); + } + zval *slot = entry.value().raw(); + if (!hashZval(slot, h, depth + 1)) { + return false; + } + } + return true; + } + case IS_OBJECT: { + zend_object *obj = Z_OBJ_P(value); + if (cePlan(obj->ce).kind == CE_STRUCTURAL) { + Hash128 inner; + if (!hashObject(obj, inner, depth + 1)) { + return false; + } + mixByte(h, 11); + mixU64(h, inner.a); + mixU64(h, inner.b); + return true; + } + mixByte(h, 12); + mixU64(h, objSerial(obj)); + return true; + } + default: + return false; + } +} + +static bool hashObject(zend_object *obj, Hash128 &out, uint32_t depth) +{ + if (UNEXPECTED(depth > HASH_DEPTH_LIMIT)) { + return false; + } + + TypeHash *cached = (TypeHash *) zend_hash_index_find_ptr(&pt_type_hashes, weakKey(obj)); + if (cached != NULL && cached->obj == obj) { + out = cached->hash; + return true; + } + + CePlan plan = cePlan(obj->ce); + Hash128 h = { FNV_OFFSET_A, FNV_OFFSET_B }; + /* The class entry pointer identifies the class uniquely within the request. */ + mixU64(h, (uint64_t) (uintptr_t) obj->ce); + + for (uint32_t i = 0; i < plan.slots; i++) { + if (!hashZval(OBJ_PROP_NUM(obj, i), h, depth + 1)) { + return false; + } + } + + TypeHash *stored = (TypeHash *) emalloc(sizeof(TypeHash)); + stored->obj = obj; + stored->hash = h; + if (zend_weakrefs_hash_add_ptr(&pt_type_hashes, obj, stored) == NULL) { + /* Already registered by a re-entrant walk; keep the existing entry. */ + efree(stored); + } + + out = h; + + return true; +} + +/* }}} */ + +/* {{{ RecursionGuard + + * While RecursionGuard::$context is non-empty, run()/runOnObjectIdentity() short-circuit + * to ErrorType, so a type operation's result depends on the call stack rather than only on + * its arguments — and runOnObjectIdentity() keys on spl_object_id(), which memoization + * itself perturbs by handing back shared instances. The memo is therefore bypassed whole + * while a guard is active: entries are only ever produced and consumed with an empty + * context, where the operations are pure functions of their arguments. Failing to read the + * guard disables the memo rather than risking an unsound entry. */ + +static bool guardActive() +{ + if (UNEXPECTED(!pt_guard_resolved)) { + pt_guard_resolved = true; + pt_guard_unavailable = true; + + zend_class_entry *ce = pt_class(PT_CLASS_RECURSION_GUARD); + if (ce == NULL) { + return true; + } + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, "context", sizeof("context") - 1); + if (info == NULL || (info->flags & ZEND_ACC_STATIC) == 0) { + return true; + } + + pt_guard_ce = ce; + pt_guard_offset = info->offset; + pt_guard_unavailable = false; + } + + if (UNEXPECTED(pt_guard_unavailable)) { + return true; + } + + if (UNEXPECTED(CE_STATIC_MEMBERS(pt_guard_ce) == NULL)) { + zend_class_init_statics(pt_guard_ce); + } + + zval *context = &CE_STATIC_MEMBERS(pt_guard_ce)[pt_guard_offset]; + ZVAL_DEREF(context); + + return Z_TYPE_P(context) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(context)) > 0; +} + +/* }}} */ + + + +/* {{{ the memo */ + +static void memoEntryDtor(zval *zv) +{ + MemoEntry *entry = (MemoEntry *) Z_PTR_P(zv); + OBJ_RELEASE(entry->result); + efree(entry); +} + +static void typeHashDtor(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +static void objSerialDtor(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +/* Mirrors PHPStan\Type\TypeCombinatorCache. */ +class TypeCombinatorCache +{ +public: + enum Op : uint8_t { + UNION = 1, + INTERSECT = 2, + REMOVE = 3, + }; + + static void run(INTERNAL_FUNCTION_PARAMETERS, Op op, zend_function *fn, zval *args, uint32_t argc) + { + uint8_t key[2 + MEMO_ARGS_LIMIT * sizeof(Hash128)]; + size_t keyLen = 0; + bool memoizable = argc > 0 && argc <= MEMO_ARGS_LIMIT && !guardActive(); + + if (memoizable) { + key[keyLen++] = (uint8_t) op; + key[keyLen++] = (uint8_t) argc; + for (uint32_t i = 0; i < argc; i++) { + zval *arg = &args[i]; + ZVAL_DEREF(arg); + Hash128 h; + if (UNEXPECTED(Z_TYPE_P(arg) != IS_OBJECT) || !hashObject(Z_OBJ_P(arg), h, 0)) { + memoizable = false; + break; + } + memcpy(key + keyLen, &h, sizeof(h)); + keyLen += sizeof(h); + } + } + + if (memoizable) { + MemoEntry *hit = (MemoEntry *) zend_hash_str_find_ptr(&pt_memo, (const char *) key, keyLen); + if (hit != NULL) { + GC_ADDREF(hit->result); + RETVAL_OBJ(hit->result); + return; + } + } + + zend_class_entry *ce = pt_class(PT_CLASS_TYPE_COMBINATOR); + if (UNEXPECTED(ce == NULL || fn == NULL)) { + return; + } + zend_call_known_function(fn, NULL, ce, return_value, argc, args, NULL); + if (UNEXPECTED(EG(exception)) || Z_TYPE_P(return_value) != IS_OBJECT) { + return; + } + + if (memoizable && zend_hash_num_elements(&pt_memo) < MEMO_ENTRIES_LIMIT) { + MemoEntry *entry = (MemoEntry *) emalloc(sizeof(MemoEntry)); + entry->result = Z_OBJ_P(return_value); + GC_ADDREF(entry->result); + if (zend_hash_str_add_ptr(&pt_memo, (const char *) key, keyLen, entry) == NULL) { + OBJ_RELEASE(entry->result); + efree(entry); + } + } + } + + static void clear() + { + if (pt_cache_inited) { + zend_hash_clean(&pt_memo); + } + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::TypeCombinatorCache; +using phpstanturbo::pt_cache_inited; +using phpstanturbo::pt_ce_kinds; +using phpstanturbo::pt_fn_do_intersect; +using phpstanturbo::pt_fn_do_remove; +using phpstanturbo::pt_fn_do_union; +using phpstanturbo::pt_memo; +using phpstanturbo::pt_obj_serials; +using phpstanturbo::pt_next_serial; +using phpstanturbo::objSerialDtor; +using phpstanturbo::pt_guard_ce; +using phpstanturbo::pt_guard_resolved; +using phpstanturbo::pt_guard_unavailable; +using phpstanturbo::pt_type_hashes; +using phpstanturbo::memoEntryDtor; +using phpstanturbo::typeHashDtor; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +/* {{{ lifecycle */ + +void pt_type_combinator_cache_rinit() +{ + if (pt_cache_inited) { + return; + } + zend_hash_init(&pt_type_hashes, 4096, NULL, typeHashDtor, 0); + zend_hash_init(&pt_memo, 4096, NULL, memoEntryDtor, 0); + zend_hash_init(&pt_ce_kinds, 128, NULL, NULL, 0); + zend_hash_init(&pt_obj_serials, 1024, NULL, objSerialDtor, 0); + pt_next_serial = 1; + pt_guard_ce = NULL; + pt_guard_resolved = false; + pt_guard_unavailable = false; + pt_cache_inited = true; +} + +void pt_type_combinator_cache_rshutdown() +{ + if (!pt_cache_inited) { + return; + } + zend_hash_destroy(&pt_memo); + zend_weakrefs_hash_destroy(&pt_type_hashes); + zend_weakrefs_hash_destroy(&pt_obj_serials); + zend_hash_destroy(&pt_ce_kinds); + pt_fn_do_union = NULL; + pt_fn_do_intersect = NULL; + pt_fn_do_remove = NULL; + pt_guard_ce = NULL; + pt_guard_resolved = false; + pt_cache_inited = false; +} + +/* }}} */ + +/* {{{ registration */ + +zend_class_entry *pt_ce_type_combinator_cache = NULL; + +static zend_function *resolveOp(zend_function **slot, const char *lcname, size_t len) +{ + if (*slot == NULL) { + zend_class_entry *ce = pt_class(PT_CLASS_TYPE_COMBINATOR); + if (ce == NULL) { + return NULL; + } + *slot = pt_find_method(ce, lcname, len); + } + return *slot; +} + +void pt_register_type_combinator_cache() +{ + static const char *TYPE_CLASS = "PHPStan\\Type\\Type"; + + reg::Class cls("PHPStanTurbo\\TypeCombinatorCache"); + + cls.method("union", reg::PublicStatic, 0, { reg::variadicObj("types", TYPE_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *types; + uint32_t count; + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('*', types, count) + ZEND_PARSE_PARAMETERS_END(); + TypeCombinatorCache::run( + INTERNAL_FUNCTION_PARAM_PASSTHRU, + TypeCombinatorCache::UNION, + resolveOp(&pt_fn_do_union, "dounion", sizeof("dounion") - 1), + types, + count); + }); + + cls.method("intersect", reg::PublicStatic, 0, { reg::variadicObj("types", TYPE_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *types; + uint32_t count; + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('*', types, count) + ZEND_PARSE_PARAMETERS_END(); + TypeCombinatorCache::run( + INTERNAL_FUNCTION_PARAM_PASSTHRU, + TypeCombinatorCache::INTERSECT, + resolveOp(&pt_fn_do_intersect, "dointersect", sizeof("dointersect") - 1), + types, + count); + }); + + cls.method("remove", reg::PublicStatic, 2, { reg::obj("fromType", TYPE_CLASS), reg::obj("typeToRemove", TYPE_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *fromType; + zval *typeToRemove; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(fromType) + Z_PARAM_OBJECT(typeToRemove) + ZEND_PARSE_PARAMETERS_END(); + zval args[2]; + ZVAL_COPY_VALUE(&args[0], fromType); + ZVAL_COPY_VALUE(&args[1], typeToRemove); + TypeCombinatorCache::run( + INTERNAL_FUNCTION_PARAM_PASSTHRU, + TypeCombinatorCache::REMOVE, + resolveOp(&pt_fn_do_remove, "doremove", sizeof("doremove") - 1), + args, + 2); + }); + + cls.method("clearCache", reg::PublicStatic, 0, {}, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + TypeCombinatorCache::clear(); + }); + + pt_ce_type_combinator_cache = cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp new file mode 100644 index 00000000000..9eb0bdf02eb --- /dev/null +++ b/turbo-ext/src/main.cpp @@ -0,0 +1,107 @@ +/* + * phpstan_turbo — optional native acceleration for PHPStan. + * + * Built on PHP-CPP: the extension skeleton, lifecycle and the (cold-path) + * Runtime class use PHP-CPP idioms. The performance-critical classes are + * registered with the raw Zend API inside onStartup and implement their + * methods on raw zvals: PHP-CPP's call trampolines allocate a + * Php::Parameters vector per call, which the boundary-economics rules of + * this extension forbid on hot paths (see turbo-ext/README.md). + */ + +/* This is the only file instantiating PHP-CPP templates (Php::Class); + * their instantiation re-triggers warnings inside PHP-CPP headers at the use + * site, outside the include-time guard in support.h — so the exemption is + * file-scoped here. Our own code in this file remains warning-clean. */ +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunknown-warning-option" +#pragma GCC diagnostic ignored "-Winconsistent-missing-override" + +#include +#include "support.h" + +/* Baked by the Makefile from git (short SHA of the last commit touching the + * watched set); "dev" outside a git checkout, which the enabler rejects. */ +#ifndef PHPSTANTURBO_VERSION +#define PHPSTANTURBO_VERSION "dev" +#endif + +/** + * Cold-path configuration entry point, implemented in idiomatic PHP-CPP. + * TurboExtensionEnabler passes a map of class names (::class constants, so + * they stay correct under the scoped phar) that the native code resolves + * lazily at run time. + */ +class Runtime : public Php::Base +{ +public: + static void configure(Php::Parameters ¶ms) + { + Php::Value map = params[0]; + if (!map.isArray()) { + throw Php::Exception("PHPStanTurbo\\Runtime::configure() expects an array"); + } + for (auto &entry : map) { + Php::Value key = entry.first; + Php::Value value = entry.second; + if (!key.isString() || !value.isString()) { + continue; + } + std::string keyStr = key; + std::string valueStr = value; + zend_string *zkey = zend_string_init(keyStr.c_str(), keyStr.size(), 0); + zend_string *zvalue = zend_string_init(valueStr.c_str(), valueStr.size(), 0); + pt_class_map_configure(zkey, zvalue); + zend_string_release(zkey); + zend_string_release(zvalue); + } + } +}; + +extern "C" { + +PHPCPP_EXPORT void *get_module() +{ + static Php::Extension extension("phpstan_turbo", PHPSTANTURBO_VERSION); + static bool initialized = false; + + if (!initialized) { + initialized = true; + + Php::Class runtime("PHPStanTurbo\\Runtime"); + runtime.method<&Runtime::configure>("configure", { + Php::ByVal("classMap", Php::Type::Array), + }); + extension.add(std::move(runtime)); + + extension.onStartup([]() { + pt_register_trinary_logic(); + pt_register_expression_type_holder(); + pt_register_conditional_expression_holder(); + pt_register_combinations_helper(); + pt_register_node_traverser(); + pt_register_scope_ops(); + pt_register_node_scanner(); + pt_register_parser_runner(); + pt_register_type_combinator_cache(); + }); + + extension.onRequest([]() { + pt_support_rinit(); + pt_node_traverser_rinit(); + pt_scope_ops_rinit(); + pt_type_combinator_cache_rinit(); + }); + + extension.onIdle([]() { + pt_scope_ops_rshutdown(); + pt_node_traverser_rshutdown(); + pt_type_combinator_cache_rshutdown(); + pt_support_rshutdown(); + }); + } + + return extension; +} + +} diff --git a/turbo-ext/src/parser/ParserEngine.h b/turbo-ext/src/parser/ParserEngine.h new file mode 100644 index 00000000000..e6322776472 --- /dev/null +++ b/turbo-ext/src/parser/ParserEngine.h @@ -0,0 +1,417 @@ +/* + * phpstanturbo::ParserEngine — native port of php-parser 5.8.0's LALR engine + * and node building (vendor/nikic/php-parser: ParserAbstract.php + + * Parser/Php8.php), structured to mirror ParserAbstract method for method: + * doParse(), getAttributes(), emitError(), the semantic-action helpers + * (handleNamespaces, parseLNumber, checkClass, ...) and the generated reduce + * actions (reduceRange1/2/3, see bin/generate-parser-actions.php). + * + * Shadowing seam: PHPStan\Parser\ParserRunner::parse($parser, $code, $errorHandler). + * The native path activates only for exact PhpParser\Parser\Php8 objects with a + * known-safe error handler; everything else falls back to $parser->parse(). + * + * ===== Value ownership discipline (read before touching anything) ===== + * + * The zv:: types spell the borrowed/owned rules of the old pn_ helpers: + * - semStack slots hold OWNED zvals; PN_SEM() hands out BORROWED zv::Ref views. + * - semValue is OWNED; assigning a zv::Val moves it in, assigning a zv::Ref + * addref-copies (mirroring PHP's `$this->semValue = $this->semStack[...]`). + * - functions taking zv::Ref borrow; functions taking zv::Val by value + * consume; functions returning zv::Val/zv::Arr transfer ownership. + * - node builders addref borrowed props and consume their `attributes`. + * - Mirrors PHP semantics exactly: PHP's `$this->semStack[$stackPos] = $v` + * keeps values alive until the slot is overwritten; so do we. + * + * "throwing" in reduce actions: PhpParser\Error cannot unwind natively (no C++ + * exceptions), so fatalError() records the abort and the engine stops after + * the action returns — the exact mirror of the PHP catch in doParse(). + */ + +#ifndef PHPSTANTURBO_PARSER_ENGINE_H +#define PHPSTANTURBO_PARSER_ENGINE_H + +#include "../support.h" +#include "../zv.h" + +#include +#include + +namespace phpstanturbo { + +/* Tokens are copied out of the PHP Token objects once per parse for locality. + * text is BORROWED from the Token object (the tokens zval array outlives the parse). */ +struct Token +{ + int id; + zend_string *text; + int line; + int pos; /* start file offset */ +}; + +/* Resolved-once-per-process data for one parser CE (the LALR tables). */ +struct Tables +{ + zend_class_entry *parserCe; + int tokenToSymbolMapSize; + int actionTableSize; + int gotoTableSize; + int invalidSymbol; + int errorSymbol; + int defaultAction; + int unexpectedTokenRule; + int YY2TBLSTATE; + int numNonLeafStates; + int numRules; /* count of ruleToLength */ + int *phpTokenToSymbol; /* dense, phpTokenToSymbolSize entries, -1 = invalid */ + int phpTokenToSymbolSize; + int *actionBase; /* numStates*2 (leaf-extended) as stored */ + int actionBaseSize; + int *action; + int *actionCheck; + int *actionDefault; + int *gotoBase; + int *gotoTable; + int *gotoCheck; + int *gotoDefault; + int *ruleToNonTerminal; + int *ruleToLength; + zend_string **symbolToName; /* borrowed persistent copies (interned dup) */ + int symbolToNameSize; + bool *dropTokens; /* indexed by php token id */ + int dropTokensSize; /* ≥ phpTokenToSymbolSize: T_BAD_CHARACTER sits above the grammar's symbol map */ +}; + +/* Per-node-class construction plan, cached per process in the class registry. */ +struct NodeClassInfo +{ + zend_class_entry *ce; + /* property slots in constructor-parameter order (excluding the parameter + * named $attributes wherever it sits); -1 entries mean "call the ctor". */ + int propSlots[16]; + uint32_t attrsSlot; /* slot of NodeAbstract::$attributes */ + int numProps; + /* The slot-write plan is derived lazily, on the first resolve that wants + * it — the registry entry must not depend on which caller saw the class + * first (isInstanceOf() resolves with useCtor=true; letting that poison + * the cache once made every later Node\Arg construction take the ctor + * path, whose attributes-last convention Arg's signature breaks). */ + enum PlanState : uint8_t { PLAN_NONE = 0, PLAN_OK, PLAN_FAILED }; + PlanState planState; +}; + +/* + * A borrowed node-property / array-element argument: a semStack slot (Ref), + * an owned temporary (Val, released by the caller's scope after the callee + * addref'd it), or PHP null (nullptr). Single pointer, zero-cost. + */ +class Borrowed +{ + zval *p; + +public: + Borrowed(std::nullptr_t) : p(NULL) {} + Borrowed(zv::Ref r) : p(r.raw()) {} + Borrowed(zv::Val &v) : p(v.raw()) {} + Borrowed(zv::Val &&v) : p(v.raw()) {} + + zval *raw() const { return p; } +}; + +/* + * $this->semValue: an owned slot assignable from an owned zv::Val (move) or a + * borrowed zv::Ref (addref copy) — the two forms `$this->semValue = ...` + * takes in the reduce actions. + */ +class SemValue +{ + zval z; + +public: + SemValue() { ZVAL_UNDEF(&z); } + SemValue(const SemValue &) = delete; + SemValue &operator=(const SemValue &) = delete; + ~SemValue() { zval_ptr_dtor(&z); } + + void operator=(zv::Val owned) + { + zval_ptr_dtor(&z); + zval v = owned.take(); + ZVAL_COPY_VALUE(&z, &v); + } + + void operator=(zv::Ref borrowed) + { + /* addref before the release: the borrow may alias the current value */ + Z_TRY_ADDREF_P(borrowed.raw()); + zval_ptr_dtor(&z); + ZVAL_COPY_VALUE(&z, borrowed.raw()); + } + + zv::Ref ref() { return zv::Ref(&z); } + zval *raw() { return &z; } +}; + +class ParserEngine +{ +public: + /* borrows the parser and error handler for the duration of one parse */ + ParserEngine(zval *parserObj, zval *errorHandler); + ~ParserEngine(); + + /* extracts the process-wide tables from the first Php8 parser seen; + * false → this parser cannot be handled natively (caller delegates) */ + static bool prepareTables(zval *parserObj); + + /* mirrors ParserAbstract::parse(): tokenize → doParse → post-process. + * false → delegate to the PHP implementation. */ + bool parse(zval *code, zval *return_value); + +private: + /* ===== engine state (mirrors ParserAbstract's protected properties) ===== */ + + zval *semStack = NULL; /* owned zvals */ + int *stateStack = NULL; + int *tokenStartStack = NULL; + int *tokenEndStack = NULL; + int stackCap = 0; + SemValue semValue; + Token *tokens = NULL; + int numTokens = 0; + int tokenPos = 0; + int errorState = 0; + bool aborted = false; /* a would-be-thrown PhpParser\Error occurred */ + zv::Val abortErrorMsg; /* string, set when aborted (UNDEF = pending zend exception) */ + zv::Val abortErrorAttrs; + + const Tables *tables = NULL; + + /* environment (borrowed for the duration of the parse) */ + zval *parserObj; + zval *errorHandler; + zval *tokensZv = NULL; /* PHP array of Token objects */ + zend_long phpVersionId = 0; + + /* per-parse trackers (native equivalents of the SplObjectStorages) */ + HashTable createdArrays; /* obj handle => zval of Array_ node (owned) */ + HashTable parenthesizedArrowFns; /* obj handle => null */ + + /* ===== the LALR loop (ParserRunner.cpp) ===== */ + + zv::Val doParse(); /* ParserAbstract::doParse() */ + bool reduce(int rule, int stackPos); /* one semantic action; false = default */ + bool reduceRange1(int rule, int stackPos); /* generated, ParserRunnerActions1.cpp */ + bool reduceRange2(int rule, int stackPos); /* generated, ParserRunnerActions2.cpp */ + bool reduceRange3(int rule, int stackPos); /* generated, ParserRunnerActions3.cpp */ + zend_string *getErrorMessage(int symbol, int state); /* ParserAbstract::getErrorMessage() */ + void growStacks(int needed); + void writeSlot(int pos, zval owned); + bool buildTokens(); /* dense Token copies of $this->tokens */ + void checkCreatedArrays(); /* the createdArrays loop in parse() */ + zv::Val makeComment(const Token *tok, int tokenPos); + + /* CommentAnnotatingVisitor port */ + struct CommentState + { + int *positions; + int count; + int index; + int pos; + bool stopped; + }; + void annotateComments(zv::Ref stmts); + bool commentEnterNode(CommentState &st, zend_object *node); + void commentWalkNode(CommentState &st, zend_object *node); + void commentWalkArray(CommentState &st, HashTable *ht); + + /* ===== attributes ===== */ + + /* getAttributes(tokenStartPos, tokenEndPos): owned array, exact key order */ + zv::Arr getAttributes(int tokenStartPos, int tokenEndPos); + zv::Arr getAttributesAt(int stackPos); /* ParserAbstract::getAttributesAt() */ + zv::Arr getAttributesForToken(int tokenPos); + + /* ===== errors ===== */ + + void emitError(const char *msg, zv::Val attributes); + void emitError(zend_string *msg, zv::Val attributes); /* msg borrowed */ + /* `throw new Error(...)`: records the abort; the caller must return */ + void fatalError(const char *msg, zv::Val attributes); + /* doParse()'s catch (Error $e) for a pending exception thrown by PHP + * code the engine called (node ctors, String_::parseEscapeSequences) */ + void abortForPendingException(); + + /* ===== class resolution + node creation (scoped-phar safe) ===== */ + + /* "Expr\\Assign"-style alias relative to PhpParser\; useCtor forces + * PHP-constructor invocation (exact semantics for ctors with logic) */ + static NodeClassInfo *resolveNodeClass(const char *alias, bool useCtor); + + /* core: props are borrowed zval* (NULL = PHP null); attributes consumed. + * On failure records the abort and returns UNDEF. */ + zv::Val createNode(const char *alias, bool useCtor, zv::Val attributes, int nprops, zval **props); + + /* new Alias(props..., $attributes) via property-slot writes */ + template + zv::Val newNode(const char *alias, zv::Val attributes, Props &&... props) + { + zval *raw[] = {Borrowed(std::forward(props)).raw()...}; + return createNode(alias, false, std::move(attributes), (int) sizeof...(Props), raw); + } + + zv::Val newNode(const char *alias, zv::Val attributes) /* zero-property nodes */ + { + return createNode(alias, false, std::move(attributes), 0, NULL); + } + + /* new Alias(props..., $attributes) through the real PHP constructor */ + template + zv::Val newNodeCtor(const char *alias, zv::Val attributes, Props &&... props) + { + zval *raw[] = {Borrowed(std::forward(props)).raw()...}; + return createNode(alias, true, std::move(attributes), (int) sizeof...(Props), raw); + } + + /* new Name(...) / new Name\FullyQualified(...) with Name::prepareName() */ + zv::Val newName(zv::Ref strOrParts, zv::Val attributes); + zv::Val newNameVariant(const char *alias, zv::Ref strOrParts, zv::Val attributes); + + /* ===== node / array access ===== */ + + static bool isInstanceOf(zv::Ref value, const char *alias); + /* borrowed property read; raw() == NULL when the property is missing */ + static zv::Ref prop(zv::Ref node, const char *name); + static void propWrite(zv::Ref node, const char *name, zv::Val value); + zv::Arr getNodeAttributes(zv::Ref node); /* $node->getAttributes() */ + void setNodeAttribute(zv::Ref node, const char *key, zv::Val value); /* $node->setAttribute() */ + /* borrowed $array[$index] element read; raw() == NULL when absent */ + static zv::Ref itemAt(zv::Ref array, zend_ulong index); + + /* $slot[] = $value on an array held in a semStack slot */ + static void pushOnto(zv::Ref arraySlot, zv::Ref value) + { + SEPARATE_ARRAY(arraySlot.raw()); + Z_TRY_ADDREF_P(value.raw()); + zend_hash_next_index_insert(Z_ARRVAL_P(arraySlot.raw()), value.raw()); + } + + static void pushOnto(zv::Ref arraySlot, zv::Val value) + { + SEPARATE_ARRAY(arraySlot.raw()); + zval v = value.take(); + zend_hash_next_index_insert(Z_ARRVAL_P(arraySlot.raw()), &v); + } + + /* array($v1) / array($v1, $v2) literals */ + static zv::Arr arrayOf(Borrowed v1) + { + zv::Arr a = zv::Arr::create(1); + a.push(zv::Ref(v1.raw())); + return a; + } + + static zv::Arr arrayOf(Borrowed v1, Borrowed v2) + { + zv::Arr a = zv::Arr::create(2); + a.push(zv::Ref(v1.raw())); + a.push(zv::Ref(v2.raw())); + return a; + } + + /* substr($str, $offset) on a semStack string slot */ + static zv::Val substr(zv::Ref str, zend_long offset); + + /* ===== per-parse trackers ===== */ + + void createdArraysAdd(zv::Ref arrayNode); + void createdArraysRemove(zv::Ref arrayNode); + void parenthesizedArrowFunctionsAdd(zv::Ref expr); + + /* ===== ParserAbstract semantic helpers (ParserRunnerHelpers.cpp) ===== */ + + zv::Val handleNamespaces(zv::Ref stmts); + int getNamespacingStyle(zv::Ref stmts); + zv::Arr getNamespaceErrorAttributes(zv::Ref nsNode); + void fixupNamespaceAttributes(zv::Ref nsNode); + zv::Val handleBuiltinTypes(zv::Ref nameNode); + static zend_long getFloatCastKind(zv::Ref castTokenText); + static zend_long getIntCastKind(zv::Ref castTokenText); + static zend_long getBoolCastKind(zv::Ref castTokenText); + static zend_long getStringCastKind(zv::Ref castTokenText); + zv::Val parseLNumber(zv::Ref str, zv::Arr attributes, bool allowInvalidOctal); + zv::Val parseNumString(zv::Ref str, zv::Val attributes); /* Int_|String_ */ + zv::Val parseDocString(zv::Ref startToken, zv::Ref contents, zv::Ref endToken, + zv::Arr attributes, zv::Val endAttributes, bool parseUnicodeEscape); + /* String_::parseEscapeSequences() on an InterpolatedStringPart's ->value, + * through the real PHP static method (used by the encapsed-string actions) */ + void parseEscapeSequencesInPart(zv::Ref partNode, const char *quote); + /* String_::parseEscapeSequences() native port; NULL after fatalError() */ + zend_string *parseEscapeSequences(zend_string *str, bool hasQuote, char quote, bool parseUnicodeEscape); + /* ParserAbstract::stripIndentation(); errors get a copy of attrsBorrowed */ + zend_string *stripIndentation(zend_string *str, zend_long indentLen, char indentChar, + bool newlineAtStart, bool newlineAtEnd, zv::Ref attrsBorrowed); + int getCommentBeforeToken(int tokenPos); /* token index or -1 */ + zv::Val maybeCreateZeroLengthNop(int tokenPos); + zv::Val maybeCreateNop(int tokenStartPos, int tokenEndPos); + zv::Val handleHaltCompiler(); + bool inlineHtmlHasLeadingNewline(int stackPos); + zv::Val fixupArrayDestructuring(zv::Ref arrayNode); + void postprocessList(zv::Ref listNode); + void fixupAlternativeElse(zv::Ref node); + void checkClassModifier(zend_long a, zend_long b, int modifierStackPos); + void checkModifier(zend_long a, zend_long b, int modifierStackPos); + void checkPropertyHookModifiers(zend_long a, zend_long b, int modifierPos); + void verifyModifier(zend_long a, zend_long b, int modifierStackPos); + void checkParam(zv::Ref param); + void checkTryCatch(zv::Ref node); + void checkNamespace(zv::Ref node); + void checkClass(zv::Ref node, int namePos); + void checkInterface(zv::Ref node, int namePos); + void checkEnum(zv::Ref node, int namePos); + void checkClassMethod(zv::Ref node, int modifierPos); + void checkClassConst(zv::Ref node, int modifierPos); + void checkUseUse(zv::Ref node, int namePos); + void checkPropertyHooksForMultiProperty(zv::Ref property, int hookPos); + void checkEmptyPropertyHookList(zv::Ref hooks, int hookPos); + void checkPropertyHook(zv::Ref hook, int paramListPos, bool hasParamList); + void checkConstantAttributes(zv::Ref node); + void checkPipeOperatorParentheses(zv::Ref expr); + void addPropertyNameToHooks(zv::Ref node); + zv::Val createExitExpr(zv::Ref nameStr, int namePos, zv::Ref args, zv::Arr attributes); + /* Name::prepareName(); NULL after fatalError() */ + zend_string *prepareName(zv::Ref nameVal); + zv::Val stringFromString(zv::Ref raw, zv::Arr attributes, bool parseUnicodeEscape); /* Scalar\String_::fromString */ + zv::Val floatFromString(zv::Ref raw, zv::Arr attributes); /* Scalar\Float_::fromString */ + void checkClassName(zv::Ref name, int namePos); + void checkImplementedInterfaces(zv::Ref interfaces); +}; + +/* Modifiers::* constants replicated (stable public API of php-parser). */ +enum +{ + PN_MOD_PUBLIC = 1, + PN_MOD_PROTECTED = 2, + PN_MOD_PRIVATE = 4, + PN_MOD_STATIC = 8, + PN_MOD_ABSTRACT = 16, + PN_MOD_FINAL = 32, + PN_MOD_READONLY = 64, + PN_MOD_PUBLIC_SET = 128, + PN_MOD_PROTECTED_SET = 256, + PN_MOD_PRIVATE_SET = 512, +}; + +} // namespace phpstanturbo + +/* + * Reduce-action idioms — only meaningful inside ParserEngine member functions + * with a `stackPos` local (the generated reduceRange* bodies): + * PN_SEM(n, m) $this->semStack[$stackPos - (n - m)] borrowed zv::Ref + * PN_TOKSTART/END(n, m) the token start/end stacks at that slot + * PN_ATTRS(n, m1, m2) getAttributes() over the token span owned zv::Arr + */ +#define PN_SEM(n, m) zv::Ref(&semStack[stackPos - ((n) - (m))]) +#define PN_TOKSTART(n, m) tokenStartStack[stackPos - ((n) - (m))] +#define PN_TOKEND(n, m) tokenEndStack[stackPos - ((n) - (m))] +#define PN_ATTRS(n, m1, m2) getAttributes(PN_TOKSTART(n, m1), PN_TOKEND(n, m2)) + +#endif diff --git a/turbo-ext/src/parser/ParserRunner.cpp b/turbo-ext/src/parser/ParserRunner.cpp new file mode 100644 index 00000000000..aaf734d1dee --- /dev/null +++ b/turbo-ext/src/parser/ParserRunner.cpp @@ -0,0 +1,1319 @@ +/* + * PHPStanTurbo\ParserRunner — native LALR engine for php-parser 5.8.0. + * phpstanturbo::ParserEngine mirrors PhpParser\ParserAbstract::parse()/doParse() + * exactly; the generated reduce actions live in ParserRunnerActions*.cpp, the + * ported semantic helpers in ParserRunnerHelpers.cpp. PHP twin: + * PHPStan\Parser\ParserRunner. + * + * The parsing tables are read once per process from the first Php8 parser + * object seen (they are generated data on the object); node classes resolve + * relative to the parser CE's namespace so the scoped phar works unchanged. + */ + +#include "ParserEngine.h" +#include "ParserRunnerActionsSplit.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunknown-warning-option" +#pragma GCC diagnostic ignored "-Wunused-parameter" +extern "C" { +#include +#include /* T_COMMENT / T_DOC_COMMENT / T_WHITESPACE ids */ +} +#pragma GCC diagnostic pop + +namespace phpstanturbo { + +/* {{{ process-wide caches (NTS, CLI process lifetime) */ + +static Tables g_tables; +static bool g_tablesReady = false; +static char *g_nsPrefix = NULL; /* malloc'd */ +static size_t g_nsPrefixLen = 0; + +static HashTable g_classRegistry; /* alias => NodeClassInfo* (malloc'd) */ +static bool g_registryReady = false; + +static zend_class_entry *g_errorCe = NULL; + +static zend_string *g_key_startLine, *g_key_startTokenPos, *g_key_startFilePos, + *g_key_endLine, *g_key_endTokenPos, *g_key_endFilePos, *g_key_comments; +static bool g_keysReady = false; + +static void initAttributeKeys(void) +{ + if (g_keysReady) { + return; + } + g_key_startLine = zend_string_init("startLine", sizeof("startLine") - 1, 1); + g_key_startTokenPos = zend_string_init("startTokenPos", sizeof("startTokenPos") - 1, 1); + g_key_startFilePos = zend_string_init("startFilePos", sizeof("startFilePos") - 1, 1); + g_key_endLine = zend_string_init("endLine", sizeof("endLine") - 1, 1); + g_key_endTokenPos = zend_string_init("endTokenPos", sizeof("endTokenPos") - 1, 1); + g_key_endFilePos = zend_string_init("endFilePos", sizeof("endFilePos") - 1, 1); + g_key_comments = zend_string_init("comments", sizeof("comments") - 1, 1); + g_keysReady = true; +} + +/* }}} */ + +/* {{{ lifecycle */ + +ParserEngine::ParserEngine(zval *parserObj, zval *errorHandler) + : parserObj(parserObj), errorHandler(errorHandler) +{ + tables = &g_tables; + semValue = zv::Val::null(); + zend_hash_init(&createdArrays, 8, NULL, ZVAL_PTR_DTOR, 0); + zend_hash_init(&parenthesizedArrowFns, 8, NULL, NULL, 0); +} + +ParserEngine::~ParserEngine() +{ + if (semStack != NULL) { + for (int i = 0; i < stackCap; i++) { + if (!Z_ISUNDEF(semStack[i])) { + zval_ptr_dtor(&semStack[i]); + } + } + efree(semStack); + efree(stateStack); + efree(tokenStartStack); + efree(tokenEndStack); + } + zend_hash_destroy(&createdArrays); + zend_hash_destroy(&parenthesizedArrowFns); + if (tokens != NULL) { + efree(tokens); + } +} + +/* }}} */ + +/* {{{ reduce dispatch */ + +bool ParserEngine::reduce(int rule, int stackPos) +{ + if (rule < PN_REDUCE_SPLIT_1) { + return reduceRange1(rule, stackPos); + } + if (rule < PN_REDUCE_SPLIT_2) { + return reduceRange2(rule, stackPos); + } + return reduceRange3(rule, stackPos); +} + +/* }}} */ + +/* {{{ small value helpers */ + +zv::Val ParserEngine::substr(zv::Ref str, zend_long offset) +{ + zend_string *s = str.asString(); + zend_long slen = (zend_long) ZSTR_LEN(s); + if (offset < 0) { + offset += slen; + if (offset < 0) { + offset = 0; + } + } + if (offset > slen) { + offset = slen; + } + zval z; + ZVAL_STRINGL(&z, ZSTR_VAL(s) + offset, (size_t) (slen - offset)); + return zv::Val::adopt(z); +} + +/* }}} */ + +/* {{{ attributes */ + +zv::Arr ParserEngine::getAttributes(int tokenStartPos, int tokenEndPos) +{ + initAttributeKeys(); + const Token *startToken = &tokens[tokenStartPos]; + const Token *afterEndToken = &tokens[tokenEndPos + 1]; + zv::Arr attrs = zv::Arr::create(6); + HashTable *ht = attrs.table(); + zval v; + ZVAL_LONG(&v, startToken->line); + zend_hash_add_new(ht, g_key_startLine, &v); + ZVAL_LONG(&v, tokenStartPos); + zend_hash_add_new(ht, g_key_startTokenPos, &v); + ZVAL_LONG(&v, startToken->pos); + zend_hash_add_new(ht, g_key_startFilePos, &v); + ZVAL_LONG(&v, afterEndToken->line); + zend_hash_add_new(ht, g_key_endLine, &v); + ZVAL_LONG(&v, tokenEndPos); + zend_hash_add_new(ht, g_key_endTokenPos, &v); + ZVAL_LONG(&v, afterEndToken->pos - 1); + zend_hash_add_new(ht, g_key_endFilePos, &v); + return attrs; +} + +zv::Arr ParserEngine::getAttributesAt(int stackPos) +{ + return getAttributes(tokenStartStack[stackPos], tokenEndStack[stackPos]); +} + +zv::Arr ParserEngine::getAttributesForToken(int tokenPos) +{ + if (tokenPos < numTokens - 1) { + return getAttributes(tokenPos, tokenPos); + } + initAttributeKeys(); + const Token *token = &tokens[tokenPos]; + zv::Arr attrs = zv::Arr::create(6); + HashTable *ht = attrs.table(); + zval v; + ZVAL_LONG(&v, token->line); + zend_hash_add_new(ht, g_key_startLine, &v); + ZVAL_LONG(&v, tokenPos); + zend_hash_add_new(ht, g_key_startTokenPos, &v); + ZVAL_LONG(&v, token->pos); + zend_hash_add_new(ht, g_key_startFilePos, &v); + ZVAL_LONG(&v, token->line); + zend_hash_add_new(ht, g_key_endLine, &v); + ZVAL_LONG(&v, tokenPos); + zend_hash_add_new(ht, g_key_endTokenPos, &v); + ZVAL_LONG(&v, token->pos); + zend_hash_add_new(ht, g_key_endFilePos, &v); + return attrs; +} + +/* }}} */ + +/* {{{ node property access */ + +zv::Ref ParserEngine::prop(zv::Ref node, const char *name) +{ + return zv::ObjRef(node.raw()).prop(name, strlen(name)); +} + +void ParserEngine::propWrite(zv::Ref node, const char *name, zv::Val value) +{ + zv::Ref slot = prop(node, name); + if (slot.raw() == NULL) { + return; /* the Val releases the value */ + } + zval_ptr_dtor(slot.raw()); + zval v = value.take(); + ZVAL_COPY_VALUE(slot.raw(), &v); +} + +zv::Arr ParserEngine::getNodeAttributes(zv::Ref node) +{ + zv::Ref attrs = prop(node, "attributes"); + if (attrs.raw() == NULL || !attrs.isArray()) { + return zv::Arr::create(0); + } + zv::Arr copy; + ZVAL_COPY(copy.raw(), attrs.raw()); + return copy; +} + +void ParserEngine::setNodeAttribute(zv::Ref node, const char *key, zv::Val value) +{ + zv::Ref attrs = prop(node, "attributes"); + if (attrs.raw() == NULL || !attrs.isArray()) { + return; /* the Val releases the value */ + } + SEPARATE_ARRAY(attrs.raw()); + zval v = value.take(); + zend_hash_str_update(Z_ARRVAL_P(attrs.raw()), key, strlen(key), &v); +} + +zv::Ref ParserEngine::itemAt(zv::Ref array, zend_ulong index) +{ + return zv::Ref(zend_hash_index_find(Z_ARRVAL_P(array.raw()), index)); +} + +/* }}} */ + +/* {{{ class resolution + node creation */ + +static zend_class_entry *lookupClassPrefixed(const char *relative, size_t relativeLen) +{ + size_t len = g_nsPrefixLen + relativeLen; + char *name = (char *) emalloc(len + 1); + memcpy(name, g_nsPrefix, g_nsPrefixLen); + memcpy(name + g_nsPrefixLen, relative, relativeLen); + name[len] = '\0'; + zend_string *zname = zend_string_init(name, len, 0); + efree(name); + zend_class_entry *ce = zend_lookup_class(zname); + zend_string_release(zname); + return ce; +} + +NodeClassInfo *ParserEngine::resolveNodeClass(const char *alias, bool useCtor) +{ + if (!g_registryReady) { + zend_hash_init(&g_classRegistry, 64, NULL, NULL, 1); + g_registryReady = true; + } + size_t aliasLen = strlen(alias); + zval *cached = zend_hash_str_find(&g_classRegistry, alias, aliasLen); + NodeClassInfo *cls; + if (cached != NULL) { + cls = (NodeClassInfo *) Z_PTR_P(cached); + } else { + /* try PhpParser\Node\, then PhpParser\ */ + zend_class_entry *ce = NULL; + { + const char *nodePrefix = "PhpParser\\Node\\"; + size_t rl = strlen(nodePrefix) + aliasLen; + char *rel = (char *) emalloc(rl + 1); + memcpy(rel, nodePrefix, strlen(nodePrefix)); + memcpy(rel + strlen(nodePrefix), alias, aliasLen + 1); + ce = lookupClassPrefixed(rel, rl); + efree(rel); + } + if (ce == NULL) { + const char *plainPrefix = "PhpParser\\"; + size_t rl = strlen(plainPrefix) + aliasLen; + char *rel = (char *) emalloc(rl + 1); + memcpy(rel, plainPrefix, strlen(plainPrefix)); + memcpy(rel + strlen(plainPrefix), alias, aliasLen + 1); + ce = lookupClassPrefixed(rel, rl); + efree(rel); + } + if (ce == NULL) { + return NULL; + } + + cls = (NodeClassInfo *) malloc(sizeof(NodeClassInfo)); + memset(cls, 0, sizeof(*cls)); + cls->ce = ce; + cls->attrsSlot = UINT32_MAX; + + int32_t attrsOffset = pt_instance_prop_offset(ce, "attributes", sizeof("attributes") - 1); + if (attrsOffset >= 0) { + cls->attrsSlot = (uint32_t) attrsOffset; + } + + zval ptr; + ZVAL_PTR(&ptr, cls); + zend_hash_str_add(&g_classRegistry, alias, aliasLen, &ptr); + } + + if (!useCtor && cls->planState == NodeClassInfo::PLAN_NONE) { + /* derive the property-write plan from the constructor's parameter + * names — lazily, so a useCtor=true resolve (isInstanceOf) seeing the + * class first cannot deny later slot-write callers the plan */ + cls->planState = NodeClassInfo::PLAN_FAILED; + zend_function *ctor = cls->ce->constructor; + if (ctor != NULL && ctor->type == ZEND_USER_FUNCTION && cls->attrsSlot != UINT32_MAX) { + uint32_t numArgs = ctor->op_array.num_args; + int nprops = 0; + bool ok = true; + for (uint32_t i = 0; i < numArgs; i++) { + zend_string *argName = ctor->op_array.arg_info[i].name; + if (zend_string_equals_literal(argName, "attributes")) { + continue; + } + if (nprops >= 16) { + ok = false; + break; + } + int32_t off = pt_instance_prop_offset(cls->ce, ZSTR_VAL(argName), ZSTR_LEN(argName)); + if (off < 0) { + ok = false; + break; + } + cls->propSlots[nprops++] = off; + } + if (ok) { + cls->numProps = nprops; + cls->planState = NodeClassInfo::PLAN_OK; + } + } + } + + return cls; +} + +zv::Val ParserEngine::createNode(const char *alias, bool useCtor, zv::Val attributes, int nprops, zval **props) +{ + NodeClassInfo *cls = resolveNodeClass(alias, useCtor); + if (cls == NULL) { + fatalError("phpstan_turbo: unknown node class", zv::Arr::empty()); + return zv::Val(); + } + + zval attrs = attributes.take(); + zval node; + + if (!useCtor && cls->planState == NodeClassInfo::PLAN_OK && nprops == cls->numProps) { + object_init_ex(&node, cls->ce); + zend_object *zobj = Z_OBJ(node); + for (int i = 0; i < nprops; i++) { + zval *slot = OBJ_PROP(zobj, (uint32_t) cls->propSlots[i]); + zval_ptr_dtor(slot); + if (props[i] == NULL) { + ZVAL_NULL(slot); + } else { + ZVAL_COPY(slot, props[i]); + } + } + zval *attrsSlot = OBJ_PROP(zobj, cls->attrsSlot); + zval_ptr_dtor(attrsSlot); + ZVAL_COPY_VALUE(attrsSlot, &attrs); + return zv::Val::adopt(node); + } + + /* exact-semantics path: call the PHP constructor */ + object_init_ex(&node, cls->ce); + zval args[16]; + uint32_t argc = 0; + for (int i = 0; i < nprops; i++) { + if (props[i] == NULL) { + ZVAL_NULL(&args[argc++]); + } else { + ZVAL_COPY_VALUE(&args[argc++], props[i]); + } + } + ZVAL_COPY_VALUE(&args[argc++], &attrs); + zend_call_known_function(cls->ce->constructor, Z_OBJ(node), cls->ce, NULL, argc, args, NULL); + zval_ptr_dtor(&attrs); + if (EG(exception) != NULL) { + abortForPendingException(); + zval_ptr_dtor(&node); + return zv::Val(); + } + return zv::Val::adopt(node); +} + +/* + * Mirrors doParse()'s catch (Error $e): a pending PhpParser\Error becomes the + * aborted/abortErrorMsg channel (so it reaches the error handler with a + * startLine), any other pending exception just marks the parse aborted and + * propagates. Callers that invoke PHP code which may throw PhpParser\Error + * (node constructors, String_::parseEscapeSequences) must route through this. + */ +void ParserEngine::abortForPendingException() +{ + zend_object *ex = EG(exception); + if (g_errorCe != NULL && instanceof_function(ex->ce, g_errorCe)) { + zval rvMsg, rvAttrs; + ZVAL_UNDEF(&rvMsg); + ZVAL_UNDEF(&rvAttrs); + zend_function *getMsg = pt_find_method(ex->ce, "getrawmessage", sizeof("getrawmessage") - 1); + zend_function *getAttrs = pt_find_method(ex->ce, "getattributes", sizeof("getattributes") - 1); + zend_object *exKeepAlive = ex; + GC_ADDREF(exKeepAlive); + zend_clear_exception(); + if (getMsg != NULL) { + zend_call_known_function(getMsg, exKeepAlive, exKeepAlive->ce, &rvMsg, 0, NULL, NULL); + } + if (getAttrs != NULL) { + zend_call_known_function(getAttrs, exKeepAlive, exKeepAlive->ce, &rvAttrs, 0, NULL, NULL); + } + OBJ_RELEASE(exKeepAlive); + aborted = true; + abortErrorMsg = zv::Val::adopt(rvMsg); + abortErrorAttrs = zv::Val::adopt(rvAttrs); + } else { + aborted = true; /* propagate the pending exception */ + } +} + +bool ParserEngine::isInstanceOf(zv::Ref value, const char *alias) +{ + if (!value.isObject()) { + return false; + } + NodeClassInfo *cls = resolveNodeClass(alias, true); + if (cls == NULL) { + return false; + } + return instanceof_function(Z_OBJCE_P(value.raw()), cls->ce); +} + +/* }}} */ + +/* {{{ errors */ + +static zval makeErrorObject(zend_string *msg, zval *attrsBorrowed) +{ + zval error; + object_init_ex(&error, g_errorCe); + zval args[2]; + ZVAL_STR(&args[0], msg); + ZVAL_COPY_VALUE(&args[1], attrsBorrowed); + zend_call_known_function(g_errorCe->constructor, Z_OBJ(error), g_errorCe, NULL, 2, args, NULL); + return error; +} + +void ParserEngine::emitError(zend_string *msg, zv::Val attributes) +{ + zval error = makeErrorObject(msg, attributes.raw()); + attributes.release(); + if (EG(exception) != NULL) { + aborted = true; + zval_ptr_dtor(&error); + return; + } + zend_object *handler = Z_OBJ_P(errorHandler); + zend_function *fn = pt_find_method(handler->ce, "handleerror", sizeof("handleerror") - 1); + if (fn != NULL) { + zend_call_known_function(fn, handler, handler->ce, NULL, 1, &error, NULL); + } + zval_ptr_dtor(&error); + if (EG(exception) != NULL) { + /* a Throwing handler throws the Error: abort and let it propagate */ + aborted = true; + abortErrorMsg.release(); /* pending zend exception, not a caught Error */ + } +} + +void ParserEngine::emitError(const char *msg, zv::Val attributes) +{ + zend_string *zmsg = zend_string_init(msg, strlen(msg), 0); + emitError(zmsg, std::move(attributes)); + zend_string_release(zmsg); +} + +void ParserEngine::fatalError(const char *msg, zv::Val attributes) +{ + if (aborted) { + return; /* the Val releases the attributes */ + } + aborted = true; + zval m; + ZVAL_STRING(&m, msg); + abortErrorMsg = zv::Val::adopt(m); + abortErrorAttrs = std::move(attributes); +} + +/* }}} */ + +/* {{{ per-parse object-set trackers */ + +void ParserEngine::createdArraysAdd(zv::Ref arrayNode) +{ + zval copy; + ZVAL_COPY(©, arrayNode.raw()); + zend_hash_index_update(&createdArrays, Z_OBJ_HANDLE_P(arrayNode.raw()), ©); +} + +void ParserEngine::createdArraysRemove(zv::Ref arrayNode) +{ + zend_hash_index_del(&createdArrays, Z_OBJ_HANDLE_P(arrayNode.raw())); +} + +void ParserEngine::parenthesizedArrowFunctionsAdd(zv::Ref expr) +{ + zval null; + ZVAL_NULL(&null); + zend_hash_index_update(&parenthesizedArrowFns, Z_OBJ_HANDLE_P(expr.raw()), &null); +} + +/* }}} */ + +/* {{{ table extraction */ + +static bool readIntProp(zval *obj, const char *name, int *out) +{ + zv::Ref slot = zv::ObjRef(obj).prop(name, strlen(name)); + if (slot.raw() == NULL || !slot.isLong()) { + return false; + } + *out = (int) slot.asLong(); + return true; +} + +/* copies a packed-or-sparse int array property into a malloc'd dense int array */ +static bool readIntArrayProp(zval *obj, const char *name, int **out, int *outSize) +{ + zv::Ref slot = zv::ObjRef(obj).prop(name, strlen(name)); + if (slot.raw() == NULL || !slot.isArray()) { + return false; + } + HashTable *ht = slot.asArrayTable(); + zend_ulong maxKey = 0; + zend_ulong idx; + zval *v; + ZEND_HASH_FOREACH_NUM_KEY_VAL(ht, idx, v) { + (void) v; + if (idx > maxKey) { + maxKey = idx; + } + } ZEND_HASH_FOREACH_END(); + int size = (int) maxKey + 1; + if (zend_hash_num_elements(ht) == 0) { + size = 0; + } + int *arr = (int *) malloc(sizeof(int) * (size_t) (size > 0 ? size : 1)); + for (int i = 0; i < size; i++) { + arr[i] = -1; + } + ZEND_HASH_FOREACH_NUM_KEY_VAL(ht, idx, v) { + if (Z_TYPE_P(v) == IS_LONG) { + arr[idx] = (int) Z_LVAL_P(v); + } + } ZEND_HASH_FOREACH_END(); + *out = arr; + *outSize = size; + return true; +} + +static bool extractTables(zval *parserObj) +{ + zend_class_entry *ce = Z_OBJCE_P(parserObj); + + /* namespace prefix for scoped-phar-safe class resolution */ + static const char suffix[] = "PhpParser\\Parser\\Php8"; + size_t suffixLen = sizeof(suffix) - 1; + zend_string *ceName = ce->name; + if (ZSTR_LEN(ceName) < suffixLen + || memcmp(ZSTR_VAL(ceName) + ZSTR_LEN(ceName) - suffixLen, suffix, suffixLen) != 0) { + return false; + } + g_nsPrefixLen = ZSTR_LEN(ceName) - suffixLen; + g_nsPrefix = (char *) malloc(g_nsPrefixLen + 1); + memcpy(g_nsPrefix, ZSTR_VAL(ceName), g_nsPrefixLen); + g_nsPrefix[g_nsPrefixLen] = '\0'; + + Tables *t = &g_tables; + memset(t, 0, sizeof(*t)); + t->parserCe = ce; + + bool ok = readIntProp(parserObj, "tokenToSymbolMapSize", &t->tokenToSymbolMapSize) + && readIntProp(parserObj, "actionTableSize", &t->actionTableSize) + && readIntProp(parserObj, "gotoTableSize", &t->gotoTableSize) + && readIntProp(parserObj, "invalidSymbol", &t->invalidSymbol) + && readIntProp(parserObj, "errorSymbol", &t->errorSymbol) + && readIntProp(parserObj, "defaultAction", &t->defaultAction) + && readIntProp(parserObj, "unexpectedTokenRule", &t->unexpectedTokenRule) + && readIntProp(parserObj, "YY2TBLSTATE", &t->YY2TBLSTATE) + && readIntProp(parserObj, "numNonLeafStates", &t->numNonLeafStates); + int unusedSize; + ok = ok + && readIntArrayProp(parserObj, "phpTokenToSymbol", &t->phpTokenToSymbol, &t->phpTokenToSymbolSize) + && readIntArrayProp(parserObj, "actionBase", &t->actionBase, &t->actionBaseSize) + && readIntArrayProp(parserObj, "action", &t->action, &unusedSize) + && readIntArrayProp(parserObj, "actionCheck", &t->actionCheck, &unusedSize) + && readIntArrayProp(parserObj, "actionDefault", &t->actionDefault, &unusedSize) + && readIntArrayProp(parserObj, "gotoBase", &t->gotoBase, &unusedSize) + && readIntArrayProp(parserObj, "goto", &t->gotoTable, &unusedSize) + && readIntArrayProp(parserObj, "gotoCheck", &t->gotoCheck, &unusedSize) + && readIntArrayProp(parserObj, "gotoDefault", &t->gotoDefault, &unusedSize) + && readIntArrayProp(parserObj, "ruleToNonTerminal", &t->ruleToNonTerminal, &unusedSize) + && readIntArrayProp(parserObj, "ruleToLength", &t->ruleToLength, &t->numRules); + if (!ok) { + return false; + } + + /* dropTokens: bool array indexed by php token id */ + { + zv::Ref slot = zv::ObjRef(parserObj).prop("dropTokens", sizeof("dropTokens") - 1); + if (slot.raw() == NULL || !slot.isArray()) { + return false; + } + int size = t->phpTokenToSymbolSize > 1024 ? t->phpTokenToSymbolSize : 1024; + t->dropTokens = (bool *) malloc(sizeof(bool) * (size_t) size); + t->dropTokensSize = size; + memset(t->dropTokens, 0, sizeof(bool) * (size_t) size); + zend_ulong idx; + zval *v; + ZEND_HASH_FOREACH_NUM_KEY_VAL(slot.asArrayTable(), idx, v) { + (void) v; + if ((int) idx < size) { + t->dropTokens[idx] = true; + } + } ZEND_HASH_FOREACH_END(); + } + + /* symbolToName: persistent copies for error messages */ + { + zv::Ref slot = zv::ObjRef(parserObj).prop("symbolToName", sizeof("symbolToName") - 1); + if (slot.raw() == NULL || !slot.isArray()) { + return false; + } + HashTable *ht = slot.asArrayTable(); + int size = (int) zend_hash_num_elements(ht); + t->symbolToName = (zend_string **) malloc(sizeof(zend_string *) * (size_t) size); + t->symbolToNameSize = size; + for (int i = 0; i < size; i++) { + t->symbolToName[i] = NULL; + } + zend_ulong idx; + zval *v; + ZEND_HASH_FOREACH_NUM_KEY_VAL(ht, idx, v) { + if ((int) idx < size && Z_TYPE_P(v) == IS_STRING) { + t->symbolToName[idx] = zend_string_dup(Z_STR_P(v), 1); + } + } ZEND_HASH_FOREACH_END(); + } + + /* PhpParser\Error CE for emitError */ + { + const char *rel = "PhpParser\\Error"; + zend_class_entry *errorCe = lookupClassPrefixed(rel, strlen(rel)); + if (errorCe == NULL) { + return false; + } + g_errorCe = errorCe; + } + + return true; +} + +bool ParserEngine::prepareTables(zval *parserObj) +{ + if (!g_tablesReady) { + if (!extractTables(parserObj)) { + /* remember the failure by leaving g_tablesReady false; delegate */ + return false; + } + g_tablesReady = true; + } + return Z_OBJCE_P(parserObj) == g_tables.parserCe; +} + +/* }}} */ + +/* {{{ the LALR loop (ParserAbstract::doParse) */ + +#define PN_SYMBOL_NONE (-1) + +void ParserEngine::growStacks(int needed) +{ + if (needed < stackCap) { + return; + } + int newCap = stackCap < 8 ? 16 : stackCap * 2; + while (newCap <= needed) { + newCap *= 2; + } + semStack = (zval *) erealloc(semStack, sizeof(zval) * (size_t) newCap); + stateStack = (int *) erealloc(stateStack, sizeof(int) * (size_t) newCap); + tokenStartStack = (int *) erealloc(tokenStartStack, sizeof(int) * (size_t) newCap); + tokenEndStack = (int *) erealloc(tokenEndStack, sizeof(int) * (size_t) newCap); + for (int i = stackCap; i < newCap; i++) { + ZVAL_UNDEF(&semStack[i]); + stateStack[i] = 0; + tokenStartStack[i] = 0; + tokenEndStack[i] = 0; + } + stackCap = newCap; +} + +void ParserEngine::writeSlot(int pos, zval owned) +{ + growStacks(pos); + zval *slot = &semStack[pos]; + if (!Z_ISUNDEF_P(slot)) { + zval_ptr_dtor(slot); + } + ZVAL_COPY_VALUE(slot, &owned); +} + +zend_string *ParserEngine::getErrorMessage(int symbol, int state) +{ + const Tables *t = tables; + smart_str msg = {}; + smart_str_appends(&msg, "Syntax error, unexpected "); + if (symbol >= 0 && symbol < t->symbolToNameSize && t->symbolToName[symbol] != NULL) { + smart_str_append(&msg, t->symbolToName[symbol]); + } + + /* expected tokens (capped at 4, else omitted) */ + zend_string *expected[4]; + int numExpected = 0; + bool tooMany = false; + int base = t->actionBase[state]; + for (int sym = 0; sym < t->symbolToNameSize; sym++) { + int idx = base + sym; + bool found = (idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == sym); + if (!found && state < t->YY2TBLSTATE) { + idx = t->actionBase[state + t->numNonLeafStates] + sym; + found = (idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == sym); + } + if (!found) { + continue; + } + if (t->action[idx] != t->unexpectedTokenRule && t->action[idx] != t->defaultAction && sym != t->errorSymbol) { + if (numExpected == 4) { + tooMany = true; + break; + } + expected[numExpected++] = t->symbolToName[sym]; + } + } + if (!tooMany && numExpected > 0) { + smart_str_appends(&msg, ", expecting "); + for (int i = 0; i < numExpected; i++) { + if (i > 0) { + smart_str_appends(&msg, " or "); + } + if (expected[i] != NULL) { + smart_str_append(&msg, expected[i]); + } + } + } + smart_str_0(&msg); + return msg.s; +} + +/* returns the owned result zval (array of stmts) or UNDEF for null */ +zv::Val ParserEngine::doParse() +{ + const Tables *t = tables; + + int symbol = PN_SYMBOL_NONE; + zend_string *tokenText = NULL; /* borrowed from tokens */ + tokenPos = -1; + errorState = 0; + + int state = 0; + int stackPos = 0; + growStacks(8); + stateStack[0] = 0; + tokenEndStack[0] = 0; + + for (;;) { + int rule; + if (t->actionBase[state] == 0) { + rule = t->actionDefault[state]; + } else { + if (symbol == PN_SYMBOL_NONE) { + int tokenId; + do { + tokenPos++; + tokenId = tokens[tokenPos].id; + /* bound by dropTokensSize, not phpTokenToSymbolSize: + * T_BAD_CHARACTER (id 411 on 8.5) sits above the + * grammar's symbol map and must still be dropped */ + } while (tokenId < t->dropTokensSize && t->dropTokens[tokenId]); + + tokenText = tokens[tokenPos].text; + if (tokenId >= t->phpTokenToSymbolSize || t->phpTokenToSymbol[tokenId] < 0) { + zend_throw_exception_ex(spl_ce_RangeException, 0, + "The lexer returned an invalid token (id=%d, value=%s)", + tokenId, ZSTR_VAL(tokenText)); + return zv::Val(); + } + symbol = t->phpTokenToSymbol[tokenId]; + } + + int idx = t->actionBase[state] + symbol; + int action = 0; + bool haveAction = false; + if ((idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == symbol)) { + haveAction = true; + } else if (state < t->YY2TBLSTATE) { + idx = t->actionBase[state + t->numNonLeafStates] + symbol; + if (idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == symbol) { + haveAction = true; + } + } + if (haveAction) { + action = t->action[idx]; + } + if (haveAction && action != t->defaultAction) { + if (action > 0) { + /* shift */ + ++stackPos; + growStacks(stackPos); + state = action; + stateStack[stackPos] = state; + zval tokZv; + ZVAL_STR_COPY(&tokZv, tokenText); + writeSlot(stackPos, tokZv); + tokenStartStack[stackPos] = tokenPos; + tokenEndStack[stackPos] = tokenPos; + symbol = PN_SYMBOL_NONE; + + if (errorState != 0) { + --errorState; + } + + if (action < t->numNonLeafStates) { + continue; + } + rule = action - t->numNonLeafStates; + } else { + rule = -action; + } + } else { + rule = t->actionDefault[state]; + } + } + + for (;;) { + if (rule == 0) { + /* accept */ + return zv::Val::copyOf(semValue.ref()); + } + if (rule != t->unexpectedTokenRule) { + /* reduce */ + int ruleLength = t->ruleToLength[rule]; + bool handled = reduce(rule, stackPos); + if (!handled && ruleLength > 0) { + semValue = zv::Ref(&semStack[stackPos - ruleLength + 1]); + } + if (aborted) { + if (!abortErrorMsg.isUndef()) { + /* mirror of the PHP catch (Error $e) block */ + zval attrs; + if (abortErrorAttrs.isUndef() || Z_TYPE_P(abortErrorAttrs.raw()) != IS_ARRAY) { + array_init(&attrs); + abortErrorAttrs.release(); + } else { + attrs = abortErrorAttrs.take(); + } + if (zend_hash_find(Z_ARRVAL(attrs), g_key_startLine) == NULL) { + zval line; + ZVAL_LONG(&line, tokens[tokenPos].line); + SEPARATE_ARRAY(&attrs); + zend_hash_add(Z_ARRVAL(attrs), g_key_startLine, &line); + } + aborted = false; + zend_string *msg = zend_string_copy(Z_STR_P(abortErrorMsg.raw())); + abortErrorMsg.release(); + emitError(msg, zv::Val::adopt(attrs)); + zend_string_release(msg); + } + return zv::Val(); + } + if (EG(exception) != NULL) { + return zv::Val(); + } + + /* goto - shift nonterminal */ + int lastTokenEnd = tokenEndStack[stackPos]; + stackPos -= ruleLength; + int nonTerminal = t->ruleToNonTerminal[rule]; + int idx = t->gotoBase[nonTerminal] + stateStack[stackPos]; + if (idx >= 0 && idx < t->gotoTableSize && t->gotoCheck[idx] == nonTerminal) { + state = t->gotoTable[idx]; + } else { + state = t->gotoDefault[nonTerminal]; + } + + ++stackPos; + growStacks(stackPos); + stateStack[stackPos] = state; + zval semCopy; + ZVAL_COPY(&semCopy, semValue.raw()); + writeSlot(stackPos, semCopy); + tokenEndStack[stackPos] = lastTokenEnd; + if (ruleLength == 0) { + tokenStartStack[stackPos] = tokenPos; + } + } else { + /* error */ + bool discardSymbol = false; + switch (errorState) { + case 0: { + zend_string *msg = getErrorMessage(symbol, state); + emitError(msg, getAttributesForToken(tokenPos)); + zend_string_release(msg); + if (aborted || EG(exception) != NULL) { + return zv::Val(); + } + } + ZEND_FALLTHROUGH; + case 1: + case 2: { + errorState = 3; + + for (;;) { + int idx = t->actionBase[state] + t->errorSymbol; + bool found = (idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == t->errorSymbol); + if (!found && state < t->YY2TBLSTATE) { + idx = t->actionBase[state + t->numNonLeafStates] + t->errorSymbol; + found = (idx >= 0 && idx < t->actionTableSize && t->actionCheck[idx] == t->errorSymbol); + } + int action = found ? t->action[idx] : t->defaultAction; + if (found && action != t->defaultAction) { + /* uncovered an error-expecting state */ + ++stackPos; + growStacks(stackPos); + state = action; + stateStack[stackPos] = state; + tokenStartStack[stackPos] = tokenPos; + tokenEndStack[stackPos] = tokenEndStack[stackPos - 1]; + break; + } + if (stackPos <= 0) { + return zv::Val(); + } + state = stateStack[--stackPos]; + } + break; + } + case 3: + if (symbol == 0) { + return zv::Val(); + } + symbol = PN_SYMBOL_NONE; + discardSymbol = true; + break; + } + if (discardSymbol) { + break; /* break 2 in PHP: leave the inner loop */ + } + } + + if (state < t->numNonLeafStates) { + break; + } + rule = state - t->numNonLeafStates; + } + } +} + +/* }}} */ + +/* {{{ comment annotation (CommentAnnotatingVisitor port) */ + +static int countNewlines(zend_string *s) +{ + int n = 0; + const char *p = ZSTR_VAL(s); + const char *end = p + ZSTR_LEN(s); + while ((p = (const char *) memchr(p, '\n', (size_t) (end - p))) != NULL) { + n++; + p++; + } + return n; +} + +zv::Val ParserEngine::makeComment(const Token *tok, int tokenPos) +{ + bool isDoc = tok->id == T_DOC_COMMENT; + NodeClassInfo *cls = resolveNodeClass(isDoc ? "Comment\\Doc" : "Comment", true); + zval comment; + object_init_ex(&comment, cls->ce); + zval args[7]; + ZVAL_STR_COPY(&args[0], tok->text); + ZVAL_LONG(&args[1], tok->line); + ZVAL_LONG(&args[2], tok->pos); + ZVAL_LONG(&args[3], tokenPos); + ZVAL_LONG(&args[4], tok->line + countNewlines(tok->text)); + ZVAL_LONG(&args[5], tok->pos + (int) ZSTR_LEN(tok->text) - 1); + ZVAL_LONG(&args[6], tokenPos); + zend_call_known_function(cls->ce->constructor, Z_OBJ(comment), cls->ce, NULL, 7, args, NULL); + zval_ptr_dtor(&args[0]); + return zv::Val::adopt(comment); +} + +/* returns false to skip children, sets st.stopped to end the traversal */ +bool ParserEngine::commentEnterNode(CommentState &st, zend_object *node) +{ + if (st.index >= st.count) { + st.stopped = true; + return false; + } + int nextCommentPos = st.positions[st.index]; + + pt_node_class_info *info = pt_node_class_info_for_object(node); + if (info == NULL || info->attributes_offset < 0) { + return true; + } + zval *attrs = OBJ_PROP(node, (uint32_t) info->attributes_offset); + ZVAL_DEINDIRECT(attrs); + if (Z_TYPE_P(attrs) != IS_ARRAY) { + return true; + } + zval *startPosZv = zend_hash_find(Z_ARRVAL_P(attrs), g_key_startTokenPos); + int pos = (startPosZv != NULL && Z_TYPE_P(startPosZv) == IS_LONG) ? (int) Z_LVAL_P(startPosZv) : -1; + + int oldPos = st.pos; + st.pos = pos; + if (nextCommentPos > oldPos && nextCommentPos < pos) { + zval comments; + array_init(&comments); + int scanPos = pos; + int collected = 0; + while (--scanPos >= oldPos) { + const Token *tok = &tokens[scanPos]; + if (tok->id == T_DOC_COMMENT || tok->id == T_COMMENT) { + zval comment = makeComment(tok, scanPos).take(); + zend_hash_next_index_insert(Z_ARRVAL(comments), &comment); + collected++; + continue; + } + if (tok->id != T_WHITESPACE) { + break; + } + } + if (collected > 0) { + /* array_reverse */ + zval reversed; + array_init_size(&reversed, (uint32_t) collected); + for (int i = collected - 1; i >= 0; i--) { + zval *item = zend_hash_index_find(Z_ARRVAL(comments), (zend_ulong) i); + Z_TRY_ADDREF_P(item); + zend_hash_next_index_insert(Z_ARRVAL(reversed), item); + } + SEPARATE_ARRAY(attrs); + zend_hash_update(Z_ARRVAL_P(attrs), g_key_comments, &reversed); + } + zval_ptr_dtor(&comments); + + do { + st.index++; + } while (st.index < st.count && st.positions[st.index] < st.pos); + if (st.index >= st.count) { + /* current() returning false is only detected on the NEXT enterNode in PHP */ + nextCommentPos = -1; + } else { + nextCommentPos = st.positions[st.index]; + } + } + + zval *endPosZv = zend_hash_find(Z_ARRVAL_P(attrs), g_key_endTokenPos); + int endPos = (endPosZv != NULL && Z_TYPE_P(endPosZv) == IS_LONG) ? (int) Z_LVAL_P(endPosZv) : -1; + if (nextCommentPos > endPos) { + st.pos = endPos; + return false; /* DONT_TRAVERSE_CHILDREN */ + } + return true; +} + +void ParserEngine::commentWalkNode(CommentState &st, zend_object *node) +{ + if (st.stopped) { + return; + } + if (!commentEnterNode(st, node)) { + return; + } + pt_node_class_info *info = pt_node_class_info_for_object(node); + if (info == NULL || !PT_HAS_SUBNODES(info)) { + return; + } + for (uint32_t i = 0; i < info->subnode_count; i++) { + if (st.stopped) { + return; + } + zval *sub = OBJ_PROP(node, info->subnode_offsets[i]); + ZVAL_DEINDIRECT(sub); + if (Z_TYPE_P(sub) == IS_OBJECT) { + commentWalkNode(st, Z_OBJ_P(sub)); + } else if (Z_TYPE_P(sub) == IS_ARRAY) { + commentWalkArray(st, Z_ARRVAL_P(sub)); + } + } +} + +void ParserEngine::commentWalkArray(CommentState &st, HashTable *ht) +{ + zval *item; + ZEND_HASH_FOREACH_VAL(ht, item) { + if (st.stopped) { + return; + } + ZVAL_DEREF(item); + if (Z_TYPE_P(item) == IS_OBJECT) { + commentWalkNode(st, Z_OBJ_P(item)); + } else if (Z_TYPE_P(item) == IS_ARRAY) { + commentWalkArray(st, Z_ARRVAL_P(item)); + } + } ZEND_HASH_FOREACH_END(); +} + +void ParserEngine::annotateComments(zv::Ref stmts) +{ + int numComments = 0; + for (int i = 0; i < numTokens; i++) { + if (tokens[i].id == T_COMMENT || tokens[i].id == T_DOC_COMMENT) { + numComments++; + } + } + if (numComments == 0) { + return; + } + int *positions = (int *) emalloc(sizeof(int) * (size_t) numComments); + int n = 0; + for (int i = 0; i < numTokens; i++) { + if (tokens[i].id == T_COMMENT || tokens[i].id == T_DOC_COMMENT) { + positions[n++] = i; + } + } + + CommentState st; + st.positions = positions; + st.count = numComments; + st.index = 0; + st.pos = 0; + st.stopped = false; + commentWalkArray(st, Z_ARRVAL_P(stmts.raw())); + efree(positions); +} + +/* }}} */ + +/* {{{ parse entry */ + +/* the post-parse check over arrays that kept empty-element placeholders */ +void ParserEngine::checkCreatedArrays() +{ + zval *arrayNode; + ZEND_HASH_FOREACH_VAL(&createdArrays, arrayNode) { + zv::Ref items = prop(zv::Ref(arrayNode), "items"); + if (items.raw() == NULL || !items.isArray()) { + continue; + } + zval *item; + ZEND_HASH_FOREACH_VAL(items.asArrayTable(), item) { + ZVAL_DEREF(item); + if (Z_TYPE_P(item) != IS_OBJECT) { + continue; + } + zv::Ref value = prop(zv::Ref(item), "value"); + if (value.raw() != NULL && value.isObject() && isInstanceOf(value, "Expr\\Error")) { + emitError("Cannot use empty array elements in arrays", getNodeAttributes(zv::Ref(item))); + } + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); +} + +/* copies the PHP Token objects into the dense C token array */ +bool ParserEngine::buildTokens() +{ + HashTable *ht = Z_ARRVAL_P(tokensZv); + int num = (int) zend_hash_num_elements(ht); + tokens = (Token *) emalloc(sizeof(Token) * (size_t) (num > 0 ? num : 1)); + numTokens = num; + int i = 0; + zval *tokZv; + ZEND_HASH_FOREACH_VAL(ht, tokZv) { + if (Z_TYPE_P(tokZv) != IS_OBJECT || i >= num) { + return false; + } + zv::ObjRef tok(tokZv); + zv::Ref idZv = tok.prop("id", sizeof("id") - 1); + zv::Ref textZv = tok.prop("text", sizeof("text") - 1); + zv::Ref lineZv = tok.prop("line", sizeof("line") - 1); + zv::Ref posZv = tok.prop("pos", sizeof("pos") - 1); + if (idZv.raw() == NULL || textZv.raw() == NULL || lineZv.raw() == NULL || posZv.raw() == NULL + || !idZv.isLong() || !textZv.isString() || !lineZv.isLong() || !posZv.isLong()) { + return false; + } + tokens[i].id = (int) idZv.asLong(); + tokens[i].text = textZv.asString(); + tokens[i].line = (int) lineZv.asLong(); + tokens[i].pos = (int) posZv.asLong(); + i++; + } ZEND_HASH_FOREACH_END(); + return true; +} + +/* mirrors ParserAbstract::parse(); returns false when the caller must + * delegate to the PHP implementation */ +bool ParserEngine::parse(zval *code, zval *return_value) +{ + /* tokenize via the parser's own lexer (one boundary crossing) */ + zv::Ref lexer = zv::ObjRef(parserObj).prop("lexer", sizeof("lexer") - 1); + if (lexer.raw() == NULL || !lexer.isObject()) { + return false; + } + zend_function *tokenizeFn = pt_find_method(Z_OBJCE_P(lexer.raw()), "tokenize", sizeof("tokenize") - 1); + if (tokenizeFn == NULL) { + return false; + } + zval tokensLocal; + zval args[2]; + ZVAL_COPY_VALUE(&args[0], code); + ZVAL_COPY_VALUE(&args[1], errorHandler); + zend_call_known_function(tokenizeFn, Z_OBJ_P(lexer.raw()), Z_OBJCE_P(lexer.raw()), &tokensLocal, 2, args, NULL); + if (EG(exception) != NULL) { + RETVAL_NULL(); + return true; /* exception propagates, mirroring PHP */ + } + if (Z_TYPE(tokensLocal) != IS_ARRAY) { + zval_ptr_dtor(&tokensLocal); + return false; + } + zv::Val tokensOwned = zv::Val::adopt(tokensLocal); + tokensZv = tokensOwned.raw(); + + /* $this->tokens = ... (getTokens() support) */ + propWrite(zv::Ref(parserObj), "tokens", zv::Val::copyOf(tokensOwned.ref())); + + /* phpVersion id */ + { + zv::Ref phpVersion = zv::ObjRef(parserObj).prop("phpVersion", sizeof("phpVersion") - 1); + if (phpVersion.raw() != NULL && phpVersion.isObject()) { + zv::Ref id = prop(phpVersion, "id"); + if (id.raw() != NULL && id.isLong()) { + phpVersionId = id.asLong(); + } + } + } + + if (!buildTokens()) { + return false; + } + + initAttributeKeys(); + zv::Val result = doParse(); + + if (EG(exception) == NULL) { + checkCreatedArrays(); + } + + if (!result.isUndef() && EG(exception) == NULL) { + annotateComments(result.ref()); + } + + if (result.isUndef()) { + RETVAL_NULL(); + } else { + result.intoReturnValue(return_value); + } + return true; +} + +/* }}} */ + +} // namespace phpstanturbo + +using phpstanturbo::ParserEngine; + +/* {{{ engine ABI glue: class registration + fallback delegate */ + +#include "../reg.h" + +void pt_register_parser_runner(void) +{ + reg::Class cls("PHPStanTurbo\\ParserRunner"); + + cls.method("parse", reg::PublicStatic, 3, { reg::objectArg("parser"), reg::stringArg("sourceCode"), reg::objectArg("errorHandler") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *parserObj, *code, *errorHandler; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(parserObj) + Z_PARAM_ZVAL(code) + Z_PARAM_OBJECT(errorHandler) + ZEND_PARSE_PARAMETERS_END(); + + if (Z_TYPE_P(code) == IS_STRING && ParserEngine::prepareTables(parserObj)) { + ParserEngine engine(parserObj, errorHandler); + if (engine.parse(code, return_value)) { + return; + } + } + + /* fallback: delegate to the PHP implementation */ + zend_function *parseFn = pt_find_method(Z_OBJCE_P(parserObj), "parse", sizeof("parse") - 1); + if (parseFn == NULL) { + pt_throw_should_not_happen(); + RETURN_THROWS(); + } + zval args[2]; + ZVAL_COPY_VALUE(&args[0], code); + ZVAL_COPY_VALUE(&args[1], errorHandler); + zend_call_known_function(parseFn, Z_OBJ_P(parserObj), Z_OBJCE_P(parserObj), return_value, 2, args, NULL); + }); + + cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/parser/ParserRunnerActions1.cpp b/turbo-ext/src/parser/ParserRunnerActions1.cpp new file mode 100644 index 00000000000..b2315aaa329 --- /dev/null +++ b/turbo-ext/src/parser/ParserRunnerActions1.cpp @@ -0,0 +1,967 @@ +/* GENERATED by turbo-ext/bin/generate-parser-actions.php — do not edit */ +/* + * Reduce actions for rules 1-268 of php-parser's Php8 grammar + * (vendor/nikic/php-parser/lib/PhpParser/Parser/Php8.php, initReduceCallbacks). + * Rules with a null callback have no case here; returning false makes the + * engine apply the default action. Hand-ported special cases are emitted + * verbatim from src/parser/action-overrides/.inc. + */ + +#include "ParserEngine.h" + +namespace phpstanturbo { + +bool ParserEngine::reduceRange1(int rule, int stackPos) +{ + switch (rule) { + /* rule 1 */ + case 1: { + semValue = handleNamespaces(PN_SEM(1, 1)); + return true; + } + /* rule 2 */ + case 2: { + if (!PN_SEM(2, 2).isNull()) { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + } + semValue = PN_SEM(2, 1); + return true; + } + /* rule 3 */ + case 3: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 4 (from action-overrides/aeb46c35c63d49d076d22af3cd5ef3ffc214e9dc.inc) */ + case 4: { + /* $nop = $self->maybeCreateZeroLengthNop($self->tokenPos); + * if ($nop !== null) { $self->semStack[$1][] = $nop; } $$ = $self->semStack[$1]; */ + zv::Val nop = maybeCreateZeroLengthNop(tokenPos); + if (!nop.isNull()) { + pushOnto(PN_SEM(1, 1), std::move(nop)); + } + semValue = PN_SEM(1, 1); + return true; + } + /* rule 76 */ + case 76: { + semValue = PN_SEM(1, 1); + if (semValue.ref().stringEquals("maybeCreateZeroLengthNop($self->tokenPos); + * if ($nop !== null) { $self->semStack[$1][] = $nop; } $$ = $self->semStack[$1]; */ + zv::Val nop = maybeCreateZeroLengthNop(tokenPos); + if (!nop.isNull()) { + pushOnto(PN_SEM(1, 1), std::move(nop)); + } + semValue = PN_SEM(1, 1); + return true; + } + /* rule 158 */ + case 158: { + fatalError("__HALT_COMPILER() can only be used from the outermost scope", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 159 */ + case 159: { + semValue = newNode("Stmt\\Block", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 160 */ + case 160: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(7, 5))); + sub1.set("elseifs", zv::Val::copyOf(PN_SEM(7, 6))); + sub1.set("else", zv::Val::copyOf(PN_SEM(7, 7))); + semValue = newNodeCtor("Stmt\\If_", PN_ATTRS(7, 1, 7), PN_SEM(7, 3), sub1); + return true; + } + /* rule 161 */ + case 161: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(10, 6))); + sub1.set("elseifs", zv::Val::copyOf(PN_SEM(10, 7))); + sub1.set("else", zv::Val::copyOf(PN_SEM(10, 8))); + semValue = newNodeCtor("Stmt\\If_", PN_ATTRS(10, 1, 10), PN_SEM(10, 3), sub1); + return true; + } + /* rule 162 */ + case 162: { + semValue = newNode("Stmt\\While_", PN_ATTRS(5, 1, 5), PN_SEM(5, 3), PN_SEM(5, 5)); + return true; + } + /* rule 163 */ + case 163: { + semValue = newNode("Stmt\\Do_", PN_ATTRS(7, 1, 7), PN_SEM(7, 5), PN_SEM(7, 2)); + return true; + } + /* rule 164 */ + case 164: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("init", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("cond", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("loop", zv::Val::copyOf(PN_SEM(9, 7))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(9, 9))); + semValue = newNodeCtor("Stmt\\For_", PN_ATTRS(9, 1, 9), sub1); + return true; + } + /* rule 165 */ + case 165: { + semValue = newNode("Stmt\\Switch_", PN_ATTRS(5, 1, 5), PN_SEM(5, 3), PN_SEM(5, 5)); + return true; + } + /* rule 166 */ + case 166: { + semValue = newNode("Stmt\\Break_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 167 */ + case 167: { + semValue = newNode("Stmt\\Continue_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 168 */ + case 168: { + semValue = newNode("Stmt\\Return_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 169 */ + case 169: { + semValue = newNode("Stmt\\Global_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 170 */ + case 170: { + semValue = newNode("Stmt\\Static_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 171 */ + case 171: { + semValue = newNode("Stmt\\Echo_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 172 */ + case 172: { + semValue = newNode("Stmt\\InlineHTML", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + setNodeAttribute(semValue.ref(), "hasLeadingNewline", zv::Val::boolean(inlineHtmlHasLeadingNewline(stackPos - (1 - 1)))); + return true; + } + /* rule 173 */ + case 173: { + semValue = newNode("Stmt\\Expression", PN_ATTRS(2, 1, 2), PN_SEM(2, 1)); + return true; + } + /* rule 174 */ + case 174: { + semValue = newNode("Stmt\\Unset_", PN_ATTRS(5, 1, 5), PN_SEM(5, 3)); + return true; + } + /* rule 175 */ + case 175: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("keyVar", zv::Val::null()); + sub1.set("byRef", zv::Val::copyOf(itemAt(PN_SEM(7, 5), 1))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(7, 7))); + semValue = newNodeCtor("Stmt\\Foreach_", PN_ATTRS(7, 1, 7), PN_SEM(7, 3), itemAt(PN_SEM(7, 5), 0), sub1); + return true; + } + /* rule 176 */ + case 176: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("keyVar", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("byRef", zv::Val::copyOf(itemAt(PN_SEM(9, 7), 1))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(9, 9))); + semValue = newNodeCtor("Stmt\\Foreach_", PN_ATTRS(9, 1, 9), PN_SEM(9, 3), itemAt(PN_SEM(9, 7), 0), sub1); + return true; + } + /* rule 177 */ + case 177: { + zv::Val t1 = newNode("Expr\\Error", PN_ATTRS(6, 4, 4)); + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(6, 6))); + semValue = newNodeCtor("Stmt\\Foreach_", PN_ATTRS(6, 1, 6), PN_SEM(6, 3), t1, sub1); + return true; + } + /* rule 178 */ + case 178: { + semValue = newNode("Stmt\\Declare_", PN_ATTRS(5, 1, 5), PN_SEM(5, 3), PN_SEM(5, 5)); + return true; + } + /* rule 179 */ + case 179: { + semValue = newNode("Stmt\\TryCatch", PN_ATTRS(6, 1, 6), PN_SEM(6, 3), PN_SEM(6, 5), PN_SEM(6, 6)); + checkTryCatch(semValue.ref()); + return true; + } + /* rule 180 */ + case 180: { + semValue = newNode("Stmt\\Goto_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 181 */ + case 181: { + semValue = newNode("Stmt\\Label", PN_ATTRS(2, 1, 2), PN_SEM(2, 1)); + return true; + } + /* rule 182 */ + case 182: { + semValue = zv::Val::null(); + /* means: no statement */ + return true; + } + /* rule 184 */ + case 184: { + zv::Val t1 = maybeCreateNop(PN_TOKSTART(1, 1), tokenEndStack[stackPos]); + if (t1.isUndef()) { + t1 = zv::Val::null(); /* PHP null return */ + } + semValue = std::move(t1); + return true; + } + /* rule 185 */ + case 185: { + if (isInstanceOf(PN_SEM(1, 1), "Stmt\\Block")) { + semValue = prop(PN_SEM(1, 1), "stmts"); + } else if (PN_SEM(1, 1).isNull()) { + semValue = zv::Arr::empty(); + } else { + semValue = arrayOf(PN_SEM(1, 1)); + } + return true; + } + /* rule 186 */ + case 186: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 187 */ + case 187: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 188 */ + case 188: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 189 */ + case 189: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 190 */ + case 190: { + semValue = newNode("Stmt\\Catch_", PN_ATTRS(8, 1, 8), PN_SEM(8, 3), PN_SEM(8, 4), PN_SEM(8, 7)); + return true; + } + /* rule 191 */ + case 191: { + semValue = zv::Val::null(); + return true; + } + /* rule 192 */ + case 192: { + semValue = newNode("Stmt\\Finally_", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + return true; + } + /* rule 194 */ + case 194: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 195 */ + case 195: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 196 */ + case 196: { + semValue = zv::Val::boolean(false); + return true; + } + /* rule 197 */ + case 197: { + semValue = zv::Val::boolean(true); + return true; + } + /* rule 198 */ + case 198: { + semValue = zv::Val::boolean(false); + return true; + } + /* rule 199 */ + case 199: { + semValue = zv::Val::boolean(true); + return true; + } + /* rule 200 */ + case 200: { + semValue = zv::Val::boolean(false); + return true; + } + /* rule 201 */ + case 201: { + semValue = zv::Val::boolean(true); + return true; + } + /* rule 202 */ + case 202: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 203 */ + case 203: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 205 */ + case 205: { + semValue = newNode("Node\\Identifier", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 206 */ + case 206: { + semValue = newNode("Node\\Identifier", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 207 */ + case 207: { + semValue = newNode("Node\\Identifier", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 208 */ + case 208: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("params", zv::Val::copyOf(PN_SEM(8, 5))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(8, 7))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(8, 8))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Stmt\\Function_", PN_ATTRS(8, 1, 8), PN_SEM(8, 3), sub1); + return true; + } + /* rule 209 */ + case 209: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(9, 6))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(9, 8))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(9, 9))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(9, 1))); + semValue = newNodeCtor("Stmt\\Function_", PN_ATTRS(9, 1, 9), PN_SEM(9, 4), sub1); + return true; + } + /* rule 210 */ + case 210: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("type", zv::Val::copyOf(PN_SEM(7, 1))); + sub1.set("extends", zv::Val::copyOf(PN_SEM(7, 3))); + sub1.set("implements", zv::Val::copyOf(PN_SEM(7, 4))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(7, 6))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Stmt\\Class_", PN_ATTRS(7, 1, 7), PN_SEM(7, 2), sub1); + checkClass(semValue.ref(), stackPos - (7 - 2)); + return true; + } + /* rule 211 */ + case 211: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("type", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("extends", zv::Val::copyOf(PN_SEM(8, 4))); + sub1.set("implements", zv::Val::copyOf(PN_SEM(8, 5))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(8, 7))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(8, 1))); + semValue = newNodeCtor("Stmt\\Class_", PN_ATTRS(8, 1, 8), PN_SEM(8, 3), sub1); + checkClass(semValue.ref(), stackPos - (8 - 3)); + return true; + } + /* rule 212 */ + case 212: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("extends", zv::Val::copyOf(PN_SEM(7, 4))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(7, 6))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(7, 1))); + semValue = newNodeCtor("Stmt\\Interface_", PN_ATTRS(7, 1, 7), PN_SEM(7, 3), sub1); + checkInterface(semValue.ref(), stackPos - (7 - 3)); + return true; + } + /* rule 213 */ + case 213: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(6, 5))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(6, 1))); + semValue = newNodeCtor("Stmt\\Trait_", PN_ATTRS(6, 1, 6), PN_SEM(6, 3), sub1); + return true; + } + /* rule 214 */ + case 214: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("scalarType", zv::Val::copyOf(PN_SEM(8, 4))); + sub1.set("implements", zv::Val::copyOf(PN_SEM(8, 5))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(8, 7))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(8, 1))); + semValue = newNodeCtor("Stmt\\Enum_", PN_ATTRS(8, 1, 8), PN_SEM(8, 3), sub1); + checkEnum(semValue.ref(), stackPos - (8 - 3)); + return true; + } + /* rule 215 */ + case 215: { + semValue = zv::Val::null(); + return true; + } + /* rule 216 */ + case 216: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 217 */ + case 217: { + semValue = zv::Val::null(); + return true; + } + /* rule 218 */ + case 218: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 219 */ + case 219: { + semValue = zv::Val::integer(0); + return true; + } + /* rule 222 */ + case 222: { + checkClassModifier(PN_SEM(2, 1).toLong(), PN_SEM(2, 2).toLong(), stackPos - (2 - 2)); + semValue = zv::Val::integer(PN_SEM(2, 1).toLong() | PN_SEM(2, 2).toLong()); + return true; + } + /* rule 223 */ + case 223: { + semValue = zv::Val::integer(16 /* Modifiers::ABSTRACT */); + return true; + } + /* rule 224 */ + case 224: { + semValue = zv::Val::integer(32 /* Modifiers::FINAL */); + return true; + } + /* rule 225 */ + case 225: { + semValue = zv::Val::integer(64 /* Modifiers::READONLY */); + return true; + } + /* rule 226 */ + case 226: { + semValue = zv::Val::null(); + return true; + } + /* rule 227 */ + case 227: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 228 */ + case 228: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 229 */ + case 229: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 230 */ + case 230: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 231 */ + case 231: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 233 */ + case 233: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 234 */ + case 234: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 236 */ + case 236: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 238 */ + case 238: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 239 */ + case 239: { + if (isInstanceOf(PN_SEM(1, 1), "Stmt\\Block")) { + semValue = prop(PN_SEM(1, 1), "stmts"); + } else if (PN_SEM(1, 1).isNull()) { + semValue = zv::Arr::empty(); + } else { + semValue = arrayOf(PN_SEM(1, 1)); + } + return true; + } + /* rule 240 */ + case 240: { + semValue = zv::Val::null(); + return true; + } + /* rule 241 */ + case 241: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 243 */ + case 243: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 244 */ + case 244: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 245 */ + case 245: { + semValue = newNodeCtor("Node\\DeclareItem", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 246 */ + case 246: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 247 */ + case 247: { + semValue = PN_SEM(4, 3); + return true; + } + /* rule 248 */ + case 248: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 249 */ + case 249: { + semValue = PN_SEM(5, 3); + return true; + } + /* rule 250 */ + case 250: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 251 */ + case 251: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 252 */ + case 252: { + semValue = newNode("Stmt\\Case_", PN_ATTRS(4, 1, 4), PN_SEM(4, 2), PN_SEM(4, 4)); + return true; + } + /* rule 253 */ + case 253: { + semValue = newNode("Stmt\\Case_", PN_ATTRS(3, 1, 3), nullptr, PN_SEM(3, 3)); + return true; + } + /* rule 256 */ + case 256: { + semValue = newNode("Expr\\Match_", PN_ATTRS(7, 1, 7), PN_SEM(7, 3), PN_SEM(7, 6)); + return true; + } + /* rule 257 */ + case 257: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 259 */ + case 259: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 260 */ + case 260: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 261 */ + case 261: { + semValue = newNode("Node\\MatchArm", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 262 */ + case 262: { + semValue = newNode("Node\\MatchArm", PN_ATTRS(4, 1, 4), nullptr, PN_SEM(4, 4)); + return true; + } + /* rule 263 */ + case 263: { + semValue = PN_SEM(1, 1); + return true; + } + /* rule 264 */ + case 264: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 265 */ + case 265: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 266 */ + case 266: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 267 */ + case 267: { + semValue = newNode("Stmt\\ElseIf_", PN_ATTRS(5, 1, 5), PN_SEM(5, 3), PN_SEM(5, 5)); + return true; + } + /* rule 268 */ + case 268: { + semValue = zv::Arr::empty(); + return true; + } + default: + return false; + } +} + +} // namespace phpstanturbo diff --git a/turbo-ext/src/parser/ParserRunnerActions2.cpp b/turbo-ext/src/parser/ParserRunnerActions2.cpp new file mode 100644 index 00000000000..be805039267 --- /dev/null +++ b/turbo-ext/src/parser/ParserRunnerActions2.cpp @@ -0,0 +1,899 @@ +/* GENERATED by turbo-ext/bin/generate-parser-actions.php — do not edit */ +/* + * Reduce actions for rules 269-453 of php-parser's Php8 grammar + * (vendor/nikic/php-parser/lib/PhpParser/Parser/Php8.php, initReduceCallbacks). + * Rules with a null callback have no case here; returning false makes the + * engine apply the default action. Hand-ported special cases are emitted + * verbatim from src/parser/action-overrides/.inc. + */ + +#include "ParserEngine.h" + +namespace phpstanturbo { + +bool ParserEngine::reduceRange2(int rule, int stackPos) +{ + switch (rule) { + /* rule 269 */ + case 269: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 270 */ + case 270: { + semValue = newNode("Stmt\\ElseIf_", PN_ATTRS(6, 1, 6), PN_SEM(6, 3), PN_SEM(6, 6)); + fixupAlternativeElse(semValue.ref()); + return true; + } + /* rule 271 */ + case 271: { + semValue = zv::Val::null(); + return true; + } + /* rule 272 */ + case 272: { + semValue = newNode("Stmt\\Else_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 273 */ + case 273: { + semValue = zv::Val::null(); + return true; + } + /* rule 274 */ + case 274: { + semValue = newNode("Stmt\\Else_", PN_ATTRS(3, 1, 3), PN_SEM(3, 3)); + fixupAlternativeElse(semValue.ref()); + return true; + } + /* rule 275 */ + case 275: { + semValue = arrayOf(PN_SEM(1, 1), zv::Val::boolean(false)); + return true; + } + /* rule 276 */ + case 276: { + semValue = arrayOf(PN_SEM(2, 2), zv::Val::boolean(true)); + return true; + } + /* rule 277 */ + case 277: { + semValue = arrayOf(PN_SEM(1, 1), zv::Val::boolean(false)); + return true; + } + /* rule 278 */ + case 278: { + zv::Val t1 = fixupArrayDestructuring(PN_SEM(1, 1)); + semValue = arrayOf(t1, zv::Val::boolean(false)); + return true; + } + /* rule 280 */ + case 280: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 281 */ + case 281: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 282 */ + case 282: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 283 */ + case 283: { + semValue = zv::Val::integer(0); + return true; + } + /* rule 284 */ + case 284: { + checkModifier(PN_SEM(2, 1).toLong(), PN_SEM(2, 2).toLong(), stackPos - (2 - 2)); + semValue = zv::Val::integer(PN_SEM(2, 1).toLong() | PN_SEM(2, 2).toLong()); + return true; + } + /* rule 285 */ + case 285: { + semValue = zv::Val::integer(1 /* Modifiers::PUBLIC */); + return true; + } + /* rule 286 */ + case 286: { + semValue = zv::Val::integer(2 /* Modifiers::PROTECTED */); + return true; + } + /* rule 287 */ + case 287: { + semValue = zv::Val::integer(4 /* Modifiers::PRIVATE */); + return true; + } + /* rule 288 */ + case 288: { + semValue = zv::Val::integer(128 /* Modifiers::PUBLIC_SET */); + return true; + } + /* rule 289 */ + case 289: { + semValue = zv::Val::integer(256 /* Modifiers::PROTECTED_SET */); + return true; + } + /* rule 290 */ + case 290: { + semValue = zv::Val::integer(512 /* Modifiers::PRIVATE_SET */); + return true; + } + /* rule 291 */ + case 291: { + semValue = zv::Val::integer(64 /* Modifiers::READONLY */); + return true; + } + /* rule 292 */ + case 292: { + semValue = zv::Val::integer(32 /* Modifiers::FINAL */); + return true; + } + /* rule 293 */ + case 293: { + semValue = newNode("Node\\Param", PN_ATTRS(7, 1, 7), PN_SEM(7, 6), nullptr, PN_SEM(7, 3), PN_SEM(7, 4), PN_SEM(7, 5), PN_SEM(7, 2), PN_SEM(7, 1), PN_SEM(7, 7)); + checkParam(semValue.ref()); + addPropertyNameToHooks(semValue.ref()); + return true; + } + /* rule 294 */ + case 294: { + semValue = newNode("Node\\Param", PN_ATTRS(9, 1, 9), PN_SEM(9, 6), PN_SEM(9, 8), PN_SEM(9, 3), PN_SEM(9, 4), PN_SEM(9, 5), PN_SEM(9, 2), PN_SEM(9, 1), PN_SEM(9, 9)); + checkParam(semValue.ref()); + addPropertyNameToHooks(semValue.ref()); + return true; + } + /* rule 295 */ + case 295: { + zv::Val t1 = newNode("Expr\\Error", PN_ATTRS(6, 1, 6)); + semValue = newNode("Node\\Param", PN_ATTRS(6, 1, 6), t1, nullptr, PN_SEM(6, 3), PN_SEM(6, 4), PN_SEM(6, 5), PN_SEM(6, 2), PN_SEM(6, 1), zv::Arr::empty() /* ctor default for omitted $hooks */); + return true; + } + /* rule 297 */ + case 297: { + semValue = newNode("Node\\NullableType", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 298 */ + case 298: { + semValue = newNode("Node\\UnionType", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 301 */ + case 301: { + zv::Val t1 = zv::Val::string("static", 6); + semValue = newName(t1.ref(), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 302 */ + case 302: { + semValue = handleBuiltinTypes(PN_SEM(1, 1)); + return true; + } + /* rule 303 */ + case 303: { + semValue = newNode("Node\\Identifier", PN_ATTRS(1, 1, 1), zv::Val::string("array", 5)); + return true; + } + /* rule 304 */ + case 304: { + semValue = newNode("Node\\Identifier", PN_ATTRS(1, 1, 1), zv::Val::string("callable", 8)); + return true; + } + /* rule 306 */ + case 306: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 307 */ + case 307: { + semValue = arrayOf(PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 308 */ + case 308: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 310 */ + case 310: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 311 */ + case 311: { + semValue = arrayOf(PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 312 */ + case 312: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 313 */ + case 313: { + semValue = arrayOf(PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 314 */ + case 314: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 315 */ + case 315: { + semValue = newNode("Node\\IntersectionType", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 316 */ + case 316: { + semValue = arrayOf(PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 317 */ + case 317: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 318 */ + case 318: { + semValue = newNode("Node\\IntersectionType", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 320 */ + case 320: { + semValue = newNode("Node\\NullableType", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 321 */ + case 321: { + semValue = newNode("Node\\UnionType", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 323 */ + case 323: { + semValue = zv::Val::null(); + return true; + } + /* rule 325 */ + case 325: { + semValue = zv::Val::null(); + return true; + } + /* rule 326 */ + case 326: { + semValue = PN_SEM(2, 2); + return true; + } + /* rule 327 */ + case 327: { + semValue = zv::Val::null(); + return true; + } + /* rule 328 */ + case 328: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 329 */ + case 329: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 330 */ + case 330: { + semValue = arrayOf(PN_SEM(3, 2)); + return true; + } + /* rule 331 */ + case 331: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 332 */ + case 332: { + semValue = PN_SEM(4, 2); + return true; + } + /* rule 333 */ + case 333: { + zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(4, 1, 4), PN_SEM(4, 2), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */); + semValue = arrayOf(t1); + return true; + } + /* rule 334 */ + case 334: { + semValue = arrayOf(PN_SEM(3, 2)); + return true; + } + /* rule 335 */ + case 335: { + zv::Val t1 = newNode("Node\\Arg", PN_ATTRS(3, 1, 1), PN_SEM(3, 1), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */); + semValue = arrayOf(t1, PN_SEM(3, 3)); + return true; + } + /* rule 336 */ + case 336: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 337 */ + case 337: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 338 */ + case 338: { + semValue = newNode("Node\\VariadicPlaceholder", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 339 */ + case 339: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 340 */ + case 340: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 341 */ + case 341: { + semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(true), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */); + return true; + } + /* rule 342 */ + case 342: { + semValue = newNode("Node\\Arg", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::boolean(false), zv::Val::boolean(true), nullptr /* ctor default for omitted $name */); + return true; + } + /* rule 343 */ + case 343: { + semValue = newNode("Node\\Arg", PN_ATTRS(3, 1, 3), PN_SEM(3, 3), zv::Val::boolean(false), zv::Val::boolean(false), PN_SEM(3, 1)); + return true; + } + /* rule 344 */ + case 344: { + semValue = newNode("Node\\Arg", PN_ATTRS(1, 1, 1), PN_SEM(1, 1), zv::Val::boolean(false), zv::Val::boolean(false), nullptr /* ctor default for omitted $name */); + return true; + } + /* rule 345 */ + case 345: { + semValue = PN_SEM(1, 1); + return true; + } + /* rule 347 */ + case 347: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 348 */ + case 348: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 351 */ + case 351: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 352 */ + case 352: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 353 */ + case 353: { + semValue = newNode("Node\\StaticVar", PN_ATTRS(1, 1, 1), PN_SEM(1, 1), nullptr); + return true; + } + /* rule 354 */ + case 354: { + semValue = newNode("Node\\StaticVar", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 355 */ + case 355: { + if (!PN_SEM(2, 2).isNull()) { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + } else { + semValue = PN_SEM(2, 1); + } + return true; + } + /* rule 356 */ + case 356: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 357 (from action-overrides/aeb46c35c63d49d076d22af3cd5ef3ffc214e9dc.inc) */ + case 357: { + /* $nop = $self->maybeCreateZeroLengthNop($self->tokenPos); + * if ($nop !== null) { $self->semStack[$1][] = $nop; } $$ = $self->semStack[$1]; */ + zv::Val nop = maybeCreateZeroLengthNop(tokenPos); + if (!nop.isNull()) { + pushOnto(PN_SEM(1, 1), std::move(nop)); + } + semValue = PN_SEM(1, 1); + return true; + } + /* rule 358 */ + case 358: { + semValue = newNode("Stmt\\Property", PN_ATTRS(5, 1, 5), PN_SEM(5, 2), PN_SEM(5, 4), PN_SEM(5, 3), PN_SEM(5, 1), zv::Arr::empty() /* ctor default for omitted $hooks */); + return true; + } + /* rule 359 */ + case 359: { + semValue = newNode("Stmt\\Property", PN_ATTRS(7, 1, 7), PN_SEM(7, 2), PN_SEM(7, 4), PN_SEM(7, 3), PN_SEM(7, 1), PN_SEM(7, 6)); + checkPropertyHooksForMultiProperty(semValue.ref(), stackPos - (7 - 5)); + checkEmptyPropertyHookList(PN_SEM(7, 6), stackPos - (7 - 5)); + addPropertyNameToHooks(semValue.ref()); + return true; + } + /* rule 360 */ + case 360: { + semValue = newNode("Stmt\\ClassConst", PN_ATTRS(5, 1, 5), PN_SEM(5, 4), PN_SEM(5, 2), PN_SEM(5, 1), nullptr /* ctor default for omitted $type */); + checkClassConst(semValue.ref(), stackPos - (5 - 2)); + return true; + } + /* rule 361 */ + case 361: { + semValue = newNode("Stmt\\ClassConst", PN_ATTRS(6, 1, 6), PN_SEM(6, 5), PN_SEM(6, 2), PN_SEM(6, 1), PN_SEM(6, 4)); + checkClassConst(semValue.ref(), stackPos - (6 - 2)); + return true; + } + /* rule 362 */ + case 362: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("type", zv::Val::copyOf(PN_SEM(10, 2))); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(10, 4))); + sub1.set("params", zv::Val::copyOf(PN_SEM(10, 7))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(10, 9))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(10, 10))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(10, 1))); + semValue = newNodeCtor("Stmt\\ClassMethod", PN_ATTRS(10, 1, 10), PN_SEM(10, 5), sub1); + checkClassMethod(semValue.ref(), stackPos - (10 - 2)); + return true; + } + /* rule 363 */ + case 363: { + semValue = newNode("Stmt\\TraitUse", PN_ATTRS(3, 1, 3), PN_SEM(3, 2), PN_SEM(3, 3)); + return true; + } + /* rule 364 */ + case 364: { + semValue = newNodeCtor("Stmt\\EnumCase", PN_ATTRS(5, 1, 5), PN_SEM(5, 3), PN_SEM(5, 4), PN_SEM(5, 1)); + return true; + } + /* rule 365 */ + case 365: { + semValue = zv::Val::null(); + /* will be skipped */ + return true; + } + /* rule 366 */ + case 366: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 367 */ + case 367: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 368 */ + case 368: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 369 */ + case 369: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 370 */ + case 370: { + semValue = newNodeCtor("Stmt\\TraitUseAdaptation\\Precedence", PN_ATTRS(4, 1, 4), itemAt(PN_SEM(4, 1), 0), itemAt(PN_SEM(4, 1), 1), PN_SEM(4, 3)); + return true; + } + /* rule 371 */ + case 371: { + semValue = newNodeCtor("Stmt\\TraitUseAdaptation\\Alias", PN_ATTRS(5, 1, 5), itemAt(PN_SEM(5, 1), 0), itemAt(PN_SEM(5, 1), 1), PN_SEM(5, 3), PN_SEM(5, 4)); + return true; + } + /* rule 372 */ + case 372: { + semValue = newNodeCtor("Stmt\\TraitUseAdaptation\\Alias", PN_ATTRS(4, 1, 4), itemAt(PN_SEM(4, 1), 0), itemAt(PN_SEM(4, 1), 1), PN_SEM(4, 3), nullptr); + return true; + } + /* rule 373 */ + case 373: { + semValue = newNodeCtor("Stmt\\TraitUseAdaptation\\Alias", PN_ATTRS(4, 1, 4), itemAt(PN_SEM(4, 1), 0), itemAt(PN_SEM(4, 1), 1), nullptr, PN_SEM(4, 3)); + return true; + } + /* rule 374 */ + case 374: { + semValue = newNodeCtor("Stmt\\TraitUseAdaptation\\Alias", PN_ATTRS(4, 1, 4), itemAt(PN_SEM(4, 1), 0), itemAt(PN_SEM(4, 1), 1), nullptr, PN_SEM(4, 3)); + return true; + } + /* rule 375 */ + case 375: { + semValue = arrayOf(PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 377 */ + case 377: { + semValue = arrayOf(zv::Val::null(), PN_SEM(1, 1)); + return true; + } + /* rule 378 */ + case 378: { + semValue = zv::Val::null(); + return true; + } + /* rule 381 */ + case 381: { + semValue = zv::Val::integer(0); + return true; + } + /* rule 382 */ + case 382: { + semValue = zv::Val::integer(0); + return true; + } + /* rule 385 */ + case 385: { + checkModifier(PN_SEM(2, 1).toLong(), PN_SEM(2, 2).toLong(), stackPos - (2 - 2)); + semValue = zv::Val::integer(PN_SEM(2, 1).toLong() | PN_SEM(2, 2).toLong()); + return true; + } + /* rule 386 */ + case 386: { + semValue = zv::Val::integer(1 /* Modifiers::PUBLIC */); + return true; + } + /* rule 387 */ + case 387: { + semValue = zv::Val::integer(2 /* Modifiers::PROTECTED */); + return true; + } + /* rule 388 */ + case 388: { + semValue = zv::Val::integer(4 /* Modifiers::PRIVATE */); + return true; + } + /* rule 389 */ + case 389: { + semValue = zv::Val::integer(128 /* Modifiers::PUBLIC_SET */); + return true; + } + /* rule 390 */ + case 390: { + semValue = zv::Val::integer(256 /* Modifiers::PROTECTED_SET */); + return true; + } + /* rule 391 */ + case 391: { + semValue = zv::Val::integer(512 /* Modifiers::PRIVATE_SET */); + return true; + } + /* rule 392 */ + case 392: { + semValue = zv::Val::integer(8 /* Modifiers::STATIC */); + return true; + } + /* rule 393 */ + case 393: { + semValue = zv::Val::integer(16 /* Modifiers::ABSTRACT */); + return true; + } + /* rule 394 */ + case 394: { + semValue = zv::Val::integer(32 /* Modifiers::FINAL */); + return true; + } + /* rule 395 */ + case 395: { + semValue = zv::Val::integer(64 /* Modifiers::READONLY */); + return true; + } + /* rule 397 */ + case 397: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 398 */ + case 398: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 399 */ + case 399: { + zv::Val t1 = substr(PN_SEM(1, 1), 1); + semValue = newNode("Node\\VarLikeIdentifier", PN_ATTRS(1, 1, 1), t1); + return true; + } + /* rule 400 */ + case 400: { + semValue = newNode("Node\\PropertyItem", PN_ATTRS(1, 1, 1), PN_SEM(1, 1), nullptr); + return true; + } + /* rule 401 */ + case 401: { + semValue = newNode("Node\\PropertyItem", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 402 */ + case 402: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 403 */ + case 403: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 404 */ + case 404: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 405 */ + case 405: { + semValue = PN_SEM(3, 2); + checkEmptyPropertyHookList(PN_SEM(3, 2), stackPos - (3 - 1)); + return true; + } + /* rule 406 */ + case 406: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("flags", zv::Val::copyOf(PN_SEM(5, 2))); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(5, 3))); + sub1.set("params", zv::Arr::empty()); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(5, 1))); + semValue = newNodeCtor("Node\\PropertyHook", PN_ATTRS(5, 1, 5), PN_SEM(5, 4), PN_SEM(5, 5), sub1); + checkPropertyHook(semValue.ref(), 0, false); + return true; + } + /* rule 407 */ + case 407: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("flags", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(8, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(8, 6))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(8, 1))); + semValue = newNodeCtor("Node\\PropertyHook", PN_ATTRS(8, 1, 8), PN_SEM(8, 4), PN_SEM(8, 8), sub1); + checkPropertyHook(semValue.ref(), stackPos - (8 - 5), true); + return true; + } + /* rule 408 */ + case 408: { + semValue = zv::Val::null(); + return true; + } + /* rule 409 */ + case 409: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 410 */ + case 410: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 411 */ + case 411: { + semValue = zv::Val::integer(0); + return true; + } + /* rule 412 */ + case 412: { + checkPropertyHookModifiers(PN_SEM(2, 1).toLong(), PN_SEM(2, 2).toLong(), stackPos - (2 - 2)); + semValue = zv::Val::integer(PN_SEM(2, 1).toLong() | PN_SEM(2, 2).toLong()); + return true; + } + /* rule 415 */ + case 415: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 416 */ + case 416: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 417 */ + case 417: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 420 */ + case 420: { + semValue = newNode("Expr\\Assign", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 421 */ + case 421: { + zv::Val t1 = fixupArrayDestructuring(PN_SEM(3, 1)); + semValue = newNode("Expr\\Assign", PN_ATTRS(3, 1, 3), t1, PN_SEM(3, 3)); + return true; + } + /* rule 422 */ + case 422: { + semValue = newNode("Expr\\Assign", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 423 */ + case 423: { + semValue = newNode("Expr\\AssignRef", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 4)); + return true; + } + /* rule 424 */ + case 424: { + semValue = newNode("Expr\\AssignRef", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 4)); + if (!(phpVersionId < 70000 /* PhpVersion::allowsAssignNewByReference() */)) { + emitError("Cannot assign new by reference", PN_ATTRS(4, 1, 4)); + } + return true; + } + /* rule 427 */ + case 427: { + zv::Val t1 = newName(PN_SEM(2, 1), PN_ATTRS(2, 1, 1)); + semValue = newNode("Expr\\FuncCall", PN_ATTRS(2, 1, 2), t1, PN_SEM(2, 2)); + return true; + } + /* rule 428 */ + case 428: { + semValue = newNode("Expr\\Clone_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 429 */ + case 429: { + semValue = newNode("Expr\\AssignOp\\Plus", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 430 */ + case 430: { + semValue = newNode("Expr\\AssignOp\\Minus", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 431 */ + case 431: { + semValue = newNode("Expr\\AssignOp\\Mul", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 432 */ + case 432: { + semValue = newNode("Expr\\AssignOp\\Div", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 433 */ + case 433: { + semValue = newNode("Expr\\AssignOp\\Concat", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 434 */ + case 434: { + semValue = newNode("Expr\\AssignOp\\Mod", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 435 */ + case 435: { + semValue = newNode("Expr\\AssignOp\\BitwiseAnd", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 436 */ + case 436: { + semValue = newNode("Expr\\AssignOp\\BitwiseOr", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 437 */ + case 437: { + semValue = newNode("Expr\\AssignOp\\BitwiseXor", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 438 */ + case 438: { + semValue = newNode("Expr\\AssignOp\\ShiftLeft", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 439 */ + case 439: { + semValue = newNode("Expr\\AssignOp\\ShiftRight", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 440 */ + case 440: { + semValue = newNode("Expr\\AssignOp\\Pow", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 441 */ + case 441: { + semValue = newNode("Expr\\AssignOp\\Coalesce", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 442 */ + case 442: { + semValue = newNode("Expr\\PostInc", PN_ATTRS(2, 1, 2), PN_SEM(2, 1)); + return true; + } + /* rule 443 */ + case 443: { + semValue = newNode("Expr\\PreInc", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 444 */ + case 444: { + semValue = newNode("Expr\\PostDec", PN_ATTRS(2, 1, 2), PN_SEM(2, 1)); + return true; + } + /* rule 445 */ + case 445: { + semValue = newNode("Expr\\PreDec", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 446 */ + case 446: { + semValue = newNode("Expr\\BinaryOp\\BooleanOr", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 447 */ + case 447: { + semValue = newNode("Expr\\BinaryOp\\BooleanAnd", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 448 */ + case 448: { + semValue = newNode("Expr\\BinaryOp\\LogicalOr", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 449 */ + case 449: { + semValue = newNode("Expr\\BinaryOp\\LogicalAnd", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 450 */ + case 450: { + semValue = newNode("Expr\\BinaryOp\\LogicalXor", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 451 */ + case 451: { + semValue = newNode("Expr\\BinaryOp\\BitwiseOr", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 452 */ + case 452: { + semValue = newNode("Expr\\BinaryOp\\BitwiseAnd", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 453 */ + case 453: { + semValue = newNode("Expr\\BinaryOp\\BitwiseAnd", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + default: + return false; + } +} + +} // namespace phpstanturbo diff --git a/turbo-ext/src/parser/ParserRunnerActions3.cpp b/turbo-ext/src/parser/ParserRunnerActions3.cpp new file mode 100644 index 00000000000..092b011a601 --- /dev/null +++ b/turbo-ext/src/parser/ParserRunnerActions3.cpp @@ -0,0 +1,996 @@ +/* GENERATED by turbo-ext/bin/generate-parser-actions.php — do not edit */ +/* + * Reduce actions for rules 454-649 of php-parser's Php8 grammar + * (vendor/nikic/php-parser/lib/PhpParser/Parser/Php8.php, initReduceCallbacks). + * Rules with a null callback have no case here; returning false makes the + * engine apply the default action. Hand-ported special cases are emitted + * verbatim from src/parser/action-overrides/.inc. + */ + +#include "ParserEngine.h" + +namespace phpstanturbo { + +/* array_pop($this->semValue) on a packed 0..n-1 list, with the same + * copy-on-write separation PHP performs on write. Also resets + * nNextFreeElement like array_pop. */ +static void phpArrayPop(zval *arr) +{ + SEPARATE_ARRAY(arr); + HashTable *ht = Z_ARRVAL_P(arr); + uint32_t n = zend_hash_num_elements(ht); + if (n == 0) { + return; + } + zend_hash_index_del(ht, n - 1); + ht->nNextFreeElement = (zend_long) (n - 1); + zend_hash_internal_pointer_reset(ht); +} + +bool ParserEngine::reduceRange3(int rule, int stackPos) +{ + switch (rule) { + /* rule 454 */ + case 454: { + semValue = newNode("Expr\\BinaryOp\\BitwiseXor", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 455 */ + case 455: { + semValue = newNode("Expr\\BinaryOp\\Concat", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 456 */ + case 456: { + semValue = newNode("Expr\\BinaryOp\\Plus", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 457 */ + case 457: { + semValue = newNode("Expr\\BinaryOp\\Minus", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 458 */ + case 458: { + semValue = newNode("Expr\\BinaryOp\\Mul", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 459 */ + case 459: { + semValue = newNode("Expr\\BinaryOp\\Div", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 460 */ + case 460: { + semValue = newNode("Expr\\BinaryOp\\Mod", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 461 */ + case 461: { + semValue = newNode("Expr\\BinaryOp\\ShiftLeft", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 462 */ + case 462: { + semValue = newNode("Expr\\BinaryOp\\ShiftRight", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 463 */ + case 463: { + semValue = newNode("Expr\\BinaryOp\\Pow", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 464 */ + case 464: { + semValue = newNode("Expr\\UnaryPlus", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 465 */ + case 465: { + semValue = newNode("Expr\\UnaryMinus", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 466 */ + case 466: { + semValue = newNode("Expr\\BooleanNot", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 467 */ + case 467: { + semValue = newNode("Expr\\BitwiseNot", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 468 */ + case 468: { + semValue = newNode("Expr\\BinaryOp\\Identical", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 469 */ + case 469: { + semValue = newNode("Expr\\BinaryOp\\NotIdentical", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 470 */ + case 470: { + semValue = newNode("Expr\\BinaryOp\\Equal", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 471 */ + case 471: { + semValue = newNode("Expr\\BinaryOp\\NotEqual", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 472 */ + case 472: { + semValue = newNode("Expr\\BinaryOp\\Spaceship", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 473 */ + case 473: { + semValue = newNode("Expr\\BinaryOp\\Smaller", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 474 */ + case 474: { + semValue = newNode("Expr\\BinaryOp\\SmallerOrEqual", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 475 */ + case 475: { + semValue = newNode("Expr\\BinaryOp\\Greater", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 476 */ + case 476: { + semValue = newNode("Expr\\BinaryOp\\GreaterOrEqual", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 477 */ + case 477: { + semValue = newNode("Expr\\BinaryOp\\Pipe", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + checkPipeOperatorParentheses(PN_SEM(3, 3)); + return true; + } + /* rule 478 */ + case 478: { + semValue = newNode("Expr\\Instanceof_", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 479 */ + case 479: { + semValue = PN_SEM(3, 2); + if (isInstanceOf(semValue.ref(), "Expr\\ArrowFunction")) { + parenthesizedArrowFunctionsAdd(semValue.ref()); + } + return true; + } + /* rule 480 */ + case 480: { + semValue = newNode("Expr\\Ternary", PN_ATTRS(5, 1, 5), PN_SEM(5, 1), PN_SEM(5, 3), PN_SEM(5, 5)); + return true; + } + /* rule 481 */ + case 481: { + semValue = newNode("Expr\\Ternary", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), nullptr, PN_SEM(4, 4)); + return true; + } + /* rule 482 */ + case 482: { + semValue = newNode("Expr\\BinaryOp\\Coalesce", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 483 */ + case 483: { + semValue = newNode("Expr\\Isset_", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + return true; + } + /* rule 484 */ + case 484: { + semValue = newNode("Expr\\Empty_", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + return true; + } + /* rule 485 */ + case 485: { + semValue = newNode("Expr\\Include_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::integer(1 /* Expr\Include_::TYPE_INCLUDE */)); + return true; + } + /* rule 486 */ + case 486: { + semValue = newNode("Expr\\Include_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::integer(2 /* Expr\Include_::TYPE_INCLUDE_ONCE */)); + return true; + } + /* rule 487 */ + case 487: { + semValue = newNode("Expr\\Eval_", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + return true; + } + /* rule 488 */ + case 488: { + semValue = newNode("Expr\\Include_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::integer(3 /* Expr\Include_::TYPE_REQUIRE */)); + return true; + } + /* rule 489 */ + case 489: { + semValue = newNode("Expr\\Include_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), zv::Val::integer(4 /* Expr\Include_::TYPE_REQUIRE_ONCE */)); + return true; + } + /* rule 490 */ + case 490: { + zv::Arr attrs = PN_ATTRS(2, 1, 2); + attrs.set("kind", zv::Val::integer(getIntCastKind(PN_SEM(2, 1)))); + semValue = newNode("Expr\\Cast\\Int_", std::move(attrs), PN_SEM(2, 2)); + return true; + } + /* rule 491 */ + case 491: { + zv::Arr attrs = PN_ATTRS(2, 1, 2); + attrs.set("kind", zv::Val::integer(getFloatCastKind(PN_SEM(2, 1)))); + semValue = newNode("Expr\\Cast\\Double", std::move(attrs), PN_SEM(2, 2)); + return true; + } + /* rule 492 */ + case 492: { + zv::Arr attrs = PN_ATTRS(2, 1, 2); + attrs.set("kind", zv::Val::integer(getStringCastKind(PN_SEM(2, 1)))); + semValue = newNode("Expr\\Cast\\String_", std::move(attrs), PN_SEM(2, 2)); + return true; + } + /* rule 493 */ + case 493: { + semValue = newNode("Expr\\Cast\\Array_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 494 */ + case 494: { + semValue = newNode("Expr\\Cast\\Object_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 495 */ + case 495: { + zv::Arr attrs = PN_ATTRS(2, 1, 2); + attrs.set("kind", zv::Val::integer(getBoolCastKind(PN_SEM(2, 1)))); + semValue = newNode("Expr\\Cast\\Bool_", std::move(attrs), PN_SEM(2, 2)); + return true; + } + /* rule 496 */ + case 496: { + semValue = newNode("Expr\\Cast\\Unset_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 497 */ + case 497: { + semValue = newNode("Expr\\Cast\\Void_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 498 */ + case 498: { + semValue = createExitExpr(PN_SEM(2, 1), stackPos - (2 - 1), PN_SEM(2, 2), PN_ATTRS(2, 1, 2)); + return true; + } + /* rule 499 */ + case 499: { + semValue = newNode("Expr\\ErrorSuppress", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 501 */ + case 501: { + semValue = newNode("Expr\\ShellExec", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 502 */ + case 502: { + semValue = newNode("Expr\\Print_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 503 */ + case 503: { + semValue = newNode("Expr\\Yield_", PN_ATTRS(1, 1, 1), nullptr, nullptr); + return true; + } + /* rule 504 */ + case 504: { + semValue = newNode("Expr\\Yield_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), nullptr); + return true; + } + /* rule 505 */ + case 505: { + semValue = newNode("Expr\\Yield_", PN_ATTRS(4, 1, 4), PN_SEM(4, 4), PN_SEM(4, 2)); + return true; + } + /* rule 506 */ + case 506: { + semValue = newNode("Expr\\YieldFrom", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 507 */ + case 507: { + semValue = newNode("Expr\\Throw_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 508 */ + case 508: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(false)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("params", zv::Val::copyOf(PN_SEM(8, 4))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(8, 6))); + sub1.set("expr", zv::Val::copyOf(PN_SEM(8, 8))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Expr\\ArrowFunction", PN_ATTRS(8, 1, 8), sub1); + return true; + } + /* rule 509 */ + case 509: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(true)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(9, 7))); + sub1.set("expr", zv::Val::copyOf(PN_SEM(9, 9))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Expr\\ArrowFunction", PN_ATTRS(9, 1, 9), sub1); + return true; + } + /* rule 510 */ + case 510: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(false)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("params", zv::Val::copyOf(PN_SEM(8, 4))); + sub1.set("uses", zv::Val::copyOf(PN_SEM(8, 6))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(8, 7))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(8, 8))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Expr\\Closure", PN_ATTRS(8, 1, 8), sub1); + return true; + } + /* rule 511 */ + case 511: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(true)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("uses", zv::Val::copyOf(PN_SEM(9, 7))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(9, 8))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(9, 9))); + sub1.set("attrGroups", zv::Arr::empty()); + semValue = newNodeCtor("Expr\\Closure", PN_ATTRS(9, 1, 9), sub1); + return true; + } + /* rule 512 */ + case 512: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(false)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(9, 7))); + sub1.set("expr", zv::Val::copyOf(PN_SEM(9, 9))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(9, 1))); + semValue = newNodeCtor("Expr\\ArrowFunction", PN_ATTRS(9, 1, 9), sub1); + return true; + } + /* rule 513 */ + case 513: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(true)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(10, 4))); + sub1.set("params", zv::Val::copyOf(PN_SEM(10, 6))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(10, 8))); + sub1.set("expr", zv::Val::copyOf(PN_SEM(10, 10))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(10, 1))); + semValue = newNodeCtor("Expr\\ArrowFunction", PN_ATTRS(10, 1, 10), sub1); + return true; + } + /* rule 514 */ + case 514: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(false)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(9, 3))); + sub1.set("params", zv::Val::copyOf(PN_SEM(9, 5))); + sub1.set("uses", zv::Val::copyOf(PN_SEM(9, 7))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(9, 8))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(9, 9))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(9, 1))); + semValue = newNodeCtor("Expr\\Closure", PN_ATTRS(9, 1, 9), sub1); + return true; + } + /* rule 515 */ + case 515: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("static", zv::Val::boolean(true)); + sub1.set("byRef", zv::Val::copyOf(PN_SEM(10, 4))); + sub1.set("params", zv::Val::copyOf(PN_SEM(10, 6))); + sub1.set("uses", zv::Val::copyOf(PN_SEM(10, 8))); + sub1.set("returnType", zv::Val::copyOf(PN_SEM(10, 9))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(10, 10))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(10, 1))); + semValue = newNodeCtor("Expr\\Closure", PN_ATTRS(10, 1, 10), sub1); + return true; + } + /* rule 516 */ + case 516: { + zv::Arr sub1 = zv::Arr::empty(); + sub1.set("type", zv::Val::copyOf(PN_SEM(8, 2))); + sub1.set("extends", zv::Val::copyOf(PN_SEM(8, 4))); + sub1.set("implements", zv::Val::copyOf(PN_SEM(8, 5))); + sub1.set("stmts", zv::Val::copyOf(PN_SEM(8, 7))); + sub1.set("attrGroups", zv::Val::copyOf(PN_SEM(8, 1))); + zv::Val t1 = newNodeCtor("Stmt\\Class_", PN_ATTRS(8, 1, 8), nullptr, sub1); + semValue = arrayOf(t1, PN_SEM(8, 3)); + checkClass(itemAt(semValue.ref(), 0), -1); + return true; + } + /* rule 517 */ + case 517: { + semValue = newNode("Expr\\New_", PN_ATTRS(3, 1, 3), PN_SEM(3, 2), PN_SEM(3, 3)); + return true; + } + /* rule 518 (from action-overrides/4e1822bc7002c2a89dd7a63f0c78b90f91b21da1.inc) */ + case 518: { + /* list($class, $ctorArgs) = $self->semStack[...]; $$ = new Expr\New_($class, $ctorArgs, attributes); */ + zv::Ref pair = PN_SEM(2, 2); + semValue = newNode("Expr\\New_", PN_ATTRS(2, 1, 2), itemAt(pair, 0), itemAt(pair, 1)); + return true; + } + /* rule 519 */ + case 519: { + zv::Val t1 = zv::Arr::empty(); + semValue = newNode("Expr\\New_", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), t1); + return true; + } + /* rule 522 */ + case 522: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 523 */ + case 523: { + semValue = PN_SEM(4, 3); + return true; + } + /* rule 525 */ + case 525: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 526 */ + case 526: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 527 */ + case 527: { + semValue = newNode("Node\\ClosureUse", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), PN_SEM(2, 1)); + return true; + } + /* rule 528 */ + case 528: { + semValue = newName(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 529 */ + case 529: { + semValue = newNode("Expr\\FuncCall", PN_ATTRS(2, 1, 2), PN_SEM(2, 1), PN_SEM(2, 2)); + return true; + } + /* rule 530 */ + case 530: { + semValue = newNode("Expr\\FuncCall", PN_ATTRS(2, 1, 2), PN_SEM(2, 1), PN_SEM(2, 2)); + return true; + } + /* rule 531 */ + case 531: { + semValue = newNode("Expr\\FuncCall", PN_ATTRS(2, 1, 2), PN_SEM(2, 1), PN_SEM(2, 2)); + return true; + } + /* rule 532 */ + case 532: { + semValue = newNode("Expr\\StaticCall", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3), PN_SEM(4, 4)); + return true; + } + /* rule 533 */ + case 533: { + semValue = newName(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 535 */ + case 535: { + semValue = newName(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 536 */ + case 536: { + semValue = newName(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 537 */ + case 537: { + zv::Val t1 = substr(PN_SEM(1, 1), 1); + semValue = newNameVariant("Name\\FullyQualified", t1.ref(), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 538 */ + case 538: { + zv::Val t1 = substr(PN_SEM(1, 1), 10); + semValue = newNameVariant("Name\\Relative", t1.ref(), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 541 */ + case 541: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 542 */ + case 542: { + semValue = newNode("Expr\\Error", PN_ATTRS(1, 1, 1)); + errorState = 2; + return true; + } + /* rule 545 */ + case 545: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 546 (from action-overrides/33cd4fd5bb9aa00cd570b40ef1921386414be6a8.inc) */ + case 546: { + /* $$ = array($1); foreach ($$ as $s): backtick parseEscapeSequences() on InterpolatedStringParts */ + zv::Arr arr = arrayOf(PN_SEM(1, 1)); + for (auto entry : arr.arrRef()) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "`"); + } + } + semValue = std::move(arr); + return true; + } + /* rule 547 (from action-overrides/dd5881dec8adf82f4b92c400bfedca8c43ef66b4.inc) */ + case 547: { + /* foreach ($1 as $s): backtick parseEscapeSequences() on InterpolatedStringParts; $$ = $1 */ + for (auto entry : zv::ArrRef(PN_SEM(1, 1).raw())) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "`"); + } + } + semValue = PN_SEM(1, 1); + return true; + } + /* rule 548 */ + case 548: { + semValue = zv::Arr::empty(); + return true; + } + /* rule 550 */ + case 550: { + semValue = newNode("Expr\\ConstFetch", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 551 */ + case 551: { + semValue = newNode("Scalar\\MagicConst\\Line", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 552 */ + case 552: { + semValue = newNode("Scalar\\MagicConst\\File", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 553 */ + case 553: { + semValue = newNode("Scalar\\MagicConst\\Dir", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 554 */ + case 554: { + semValue = newNode("Scalar\\MagicConst\\Class_", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 555 */ + case 555: { + semValue = newNode("Scalar\\MagicConst\\Trait_", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 556 */ + case 556: { + semValue = newNode("Scalar\\MagicConst\\Method", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 557 */ + case 557: { + semValue = newNode("Scalar\\MagicConst\\Function_", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 558 */ + case 558: { + semValue = newNode("Scalar\\MagicConst\\Namespace_", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 559 */ + case 559: { + semValue = newNode("Scalar\\MagicConst\\Property", PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 560 */ + case 560: { + semValue = newNode("Expr\\ClassConstFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 561 */ + case 561: { + semValue = newNode("Expr\\ClassConstFetch", PN_ATTRS(5, 1, 5), PN_SEM(5, 1), PN_SEM(5, 4)); + return true; + } + /* rule 562 */ + case 562: { + zv::Val t1 = newNode("Expr\\Error", PN_ATTRS(3, 3, 3)); + semValue = newNode("Expr\\ClassConstFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), t1); + errorState = 2; + return true; + } + /* rule 563 */ + case 563: { + zv::Arr attrs = PN_ATTRS(3, 1, 3); + attrs.set("kind", zv::Val::integer(2 /* Expr\Array_::KIND_SHORT */)); + semValue = newNode("Expr\\Array_", std::move(attrs), PN_SEM(3, 2)); + return true; + } + /* rule 564 */ + case 564: { + zv::Arr attrs = PN_ATTRS(4, 1, 4); + attrs.set("kind", zv::Val::integer(1 /* Expr\Array_::KIND_LONG */)); + semValue = newNode("Expr\\Array_", std::move(attrs), PN_SEM(4, 3)); + createdArraysAdd(semValue.ref()); + return true; + } + /* rule 565 */ + case 565: { + semValue = PN_SEM(1, 1); + createdArraysAdd(semValue.ref()); + return true; + } + /* rule 566 */ + case 566: { + semValue = stringFromString(PN_SEM(1, 1), PN_ATTRS(1, 1, 1), phpVersionId >= 70000 /* PhpVersion::supportsUnicodeEscapes() */); + return true; + } + /* rule 567 (from action-overrides/302966ab17149baef35dbb007abfe742d2706046.inc) */ + case 567: { + /* $attrs['kind'] = KIND_DOUBLE_QUOTED; double-quote parseEscapeSequences() on the + * InterpolatedStringParts of $2; $$ = new Scalar\InterpolatedString($2, $attrs) */ + zv::Arr attrs = PN_ATTRS(3, 1, 3); + attrs.set("kind", zv::Val::integer(2 /* Scalar\String_::KIND_DOUBLE_QUOTED */)); + for (auto entry : zv::ArrRef(PN_SEM(3, 2).raw())) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "\""); + } + } + semValue = newNode("Scalar\\InterpolatedString", std::move(attrs), PN_SEM(3, 2)); + return true; + } + /* rule 568 */ + case 568: { + semValue = parseLNumber(PN_SEM(1, 1), PN_ATTRS(1, 1, 1), phpVersionId < 70000 /* PhpVersion::allowsInvalidOctals() */); + return true; + } + /* rule 569 */ + case 569: { + semValue = floatFromString(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 573 */ + case 573: { + semValue = parseDocString(PN_SEM(3, 1), PN_SEM(3, 2), PN_SEM(3, 3), PN_ATTRS(3, 1, 3), PN_ATTRS(3, 3, 3), true); + return true; + } + /* rule 574 */ + case 574: { + zv::Val t1 = zv::Val::string("", 0); + semValue = parseDocString(PN_SEM(2, 1), t1.ref(), PN_SEM(2, 2), PN_ATTRS(2, 1, 2), PN_ATTRS(2, 2, 2), true); + return true; + } + /* rule 575 */ + case 575: { + semValue = parseDocString(PN_SEM(3, 1), PN_SEM(3, 2), PN_SEM(3, 3), PN_ATTRS(3, 1, 3), PN_ATTRS(3, 3, 3), true); + return true; + } + /* rule 576 */ + case 576: { + semValue = zv::Val::null(); + return true; + } + /* rule 579 */ + case 579: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 586 */ + case 586: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 590 */ + case 590: { + semValue = newNode("Expr\\ArrayDimFetch", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3)); + return true; + } + /* rule 592 */ + case 592: { + semValue = newNode("Expr\\MethodCall", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3), PN_SEM(4, 4)); + return true; + } + /* rule 593 */ + case 593: { + semValue = newNode("Expr\\NullsafeMethodCall", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3), PN_SEM(4, 4)); + return true; + } + /* rule 594 */ + case 594: { + semValue = zv::Val::null(); + return true; + } + /* rule 598 */ + case 598: { + semValue = newNode("Expr\\PropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 599 */ + case 599: { + semValue = newNode("Expr\\NullsafePropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 601 */ + case 601: { + semValue = newNode("Expr\\Variable", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + return true; + } + /* rule 602 */ + case 602: { + semValue = newNode("Expr\\Variable", PN_ATTRS(2, 1, 2), PN_SEM(2, 2)); + return true; + } + /* rule 603 */ + case 603: { + zv::Val t1 = newNode("Expr\\Error", PN_ATTRS(2, 1, 2)); + semValue = newNode("Expr\\Variable", PN_ATTRS(2, 1, 2), t1); + errorState = 2; + return true; + } + /* rule 604 (from action-overrides/9010e0ff64f5ffbc6b330506e2e596585337c3f7.inc) */ + case 604: { + /* $var = $1->name; $$ = \is_string($var) ? new VarLikeIdentifier($var, attributes) : $var; */ + zv::Ref var = prop(PN_SEM(1, 1), "name"); + if (var.raw() != NULL && var.isString()) { + semValue = newNode("Node\\VarLikeIdentifier", PN_ATTRS(1, 1, 1), var); + } else if (var.raw() != NULL) { + semValue = var; + } else { + semValue = zv::Val::null(); + } + return true; + } + /* rule 605 */ + case 605: { + semValue = newNode("Expr\\StaticPropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 607 */ + case 607: { + semValue = newNode("Expr\\ArrayDimFetch", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3)); + return true; + } + /* rule 608 */ + case 608: { + semValue = newNode("Expr\\PropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 609 */ + case 609: { + semValue = newNode("Expr\\NullsafePropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 610 */ + case 610: { + semValue = newNode("Expr\\StaticPropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 611 */ + case 611: { + semValue = newNode("Expr\\StaticPropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 613 */ + case 613: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 616 */ + case 616: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 618 */ + case 618: { + semValue = newNode("Expr\\Error", PN_ATTRS(1, 1, 1)); + errorState = 2; + return true; + } + /* rule 619 */ + case 619: { + semValue = newNode("Expr\\List_", PN_ATTRS(4, 1, 4), PN_SEM(4, 3)); + setNodeAttribute(semValue.ref(), "kind", zv::Val::integer(1 /* Expr\List_::KIND_LIST */)); + postprocessList(semValue.ref()); + return true; + } + /* rule 620 (from action-overrides/c903abbc90c691e24a2e00c5eeed110ffb990a89.inc) */ + case 620: { + /* $$ = $1; if ($$[count($$)-1]->value instanceof Expr\Error) array_pop($$); */ + semValue = PN_SEM(1, 1); + uint32_t n = zv::ArrRef(semValue.raw()).size(); + if (n > 0) { + zv::Ref last = itemAt(semValue.ref(), n - 1); + if (last.raw() != NULL) { + zv::Ref value = prop(last, "value"); + if (value.raw() != NULL && isInstanceOf(value, "Expr\\Error")) { + phpArrayPop(semValue.raw()); + } + } + } + return true; + } + /* rule 622 */ + case 622: { + /* do nothing -- prevent default action of $$=$self->semStack[$1]. See $551. */ + return true; + } + /* rule 623 */ + case 623: { + pushOnto(PN_SEM(3, 1), PN_SEM(3, 3)); + semValue = PN_SEM(3, 1); + return true; + } + /* rule 624 */ + case 624: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 625 */ + case 625: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(1, 1, 1), PN_SEM(1, 1), nullptr, zv::Val::boolean(false), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 626 */ + case 626: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), nullptr, zv::Val::boolean(true), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 627 */ + case 627: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(1, 1, 1), PN_SEM(1, 1), nullptr, zv::Val::boolean(false), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 628 */ + case 628: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(3, 1, 3), PN_SEM(3, 3), PN_SEM(3, 1), zv::Val::boolean(false), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 629 */ + case 629: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(4, 1, 4), PN_SEM(4, 4), PN_SEM(4, 1), zv::Val::boolean(true), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 630 */ + case 630: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(3, 1, 3), PN_SEM(3, 3), PN_SEM(3, 1), zv::Val::boolean(false), zv::Val::boolean(false) /* ctor default for omitted $unpack */); + return true; + } + /* rule 631 */ + case 631: { + semValue = newNode("Node\\ArrayItem", PN_ATTRS(2, 1, 2), PN_SEM(2, 2), nullptr, zv::Val::boolean(false), zv::Val::boolean(true)); + return true; + } + /* rule 632 (from action-overrides/0ffe3fa3e57871da42f4e4d382a75a3380e24f77.inc) */ + case 632: { + /* Error node created now to remember the position; later either reported or + * converted to a null element, depending on creation vs destructuring context. + * createEmptyElemAttributes() delegates to getAttributesForToken(); the SAME + * attrs array is shared between the Error node and the ArrayItem, as in PHP. */ + zv::Arr attrs = getAttributesForToken(tokenPos); + zv::Val attrsForError = zv::Val::copyOf(attrs.ref()); + zv::Val err = newNode("Expr\\Error", std::move(attrsForError)); + semValue = newNode("Node\\ArrayItem", std::move(attrs), err, nullptr, zv::Val::boolean(false), zv::Val::boolean(false)); + return true; + } + /* rule 633 */ + case 633: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 634 */ + case 634: { + pushOnto(PN_SEM(2, 1), PN_SEM(2, 2)); + semValue = PN_SEM(2, 1); + return true; + } + /* rule 635 */ + case 635: { + semValue = arrayOf(PN_SEM(1, 1)); + return true; + } + /* rule 636 */ + case 636: { + semValue = arrayOf(PN_SEM(2, 1), PN_SEM(2, 2)); + return true; + } + /* rule 637 */ + case 637: { + zv::Arr attrs = PN_ATTRS(1, 1, 1); + attrs.set("rawValue", zv::Val::copyOf(PN_SEM(1, 1))); + semValue = newNode("Node\\InterpolatedStringPart", std::move(attrs), PN_SEM(1, 1)); + return true; + } + /* rule 638 */ + case 638: { + semValue = newNode("Expr\\Variable", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 640 */ + case 640: { + semValue = newNode("Expr\\ArrayDimFetch", PN_ATTRS(4, 1, 4), PN_SEM(4, 1), PN_SEM(4, 3)); + return true; + } + /* rule 641 */ + case 641: { + semValue = newNode("Expr\\PropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 642 */ + case 642: { + semValue = newNode("Expr\\NullsafePropertyFetch", PN_ATTRS(3, 1, 3), PN_SEM(3, 1), PN_SEM(3, 3)); + return true; + } + /* rule 643 */ + case 643: { + semValue = newNode("Expr\\Variable", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 644 */ + case 644: { + semValue = newNode("Expr\\Variable", PN_ATTRS(3, 1, 3), PN_SEM(3, 2)); + return true; + } + /* rule 645 */ + case 645: { + semValue = newNode("Expr\\ArrayDimFetch", PN_ATTRS(6, 1, 6), PN_SEM(6, 2), PN_SEM(6, 4)); + return true; + } + /* rule 646 */ + case 646: { + semValue = PN_SEM(3, 2); + return true; + } + /* rule 647 */ + case 647: { + semValue = newNode("Scalar\\String_", PN_ATTRS(1, 1, 1), PN_SEM(1, 1)); + return true; + } + /* rule 648 */ + case 648: { + semValue = parseNumString(PN_SEM(1, 1), PN_ATTRS(1, 1, 1)); + return true; + } + /* rule 649 (from action-overrides/4d582a24c4d2e51f11f292d174b7c3141cadb3e4.inc) */ + case 649: { + /* $$ = $self->parseNumString('-' . $2, attributes) */ + zend_string *cat = zend_string_concat2("-", 1, + Z_STRVAL_P(PN_SEM(2, 2).raw()), Z_STRLEN_P(PN_SEM(2, 2).raw())); + zv::Val tmp = zv::Val::adoptString(cat); + semValue = parseNumString(tmp.ref(), PN_ATTRS(2, 1, 2)); + return true; + } + default: + return false; + } +} + +} // namespace phpstanturbo diff --git a/turbo-ext/src/parser/ParserRunnerActionsSplit.h b/turbo-ext/src/parser/ParserRunnerActionsSplit.h new file mode 100644 index 00000000000..06a0f353d5f --- /dev/null +++ b/turbo-ext/src/parser/ParserRunnerActionsSplit.h @@ -0,0 +1,13 @@ +/* GENERATED by turbo-ext/bin/generate-parser-actions.php — do not edit */ +/* Dispatch boundaries for ParserEngine::reduce() (ParserRunner.cpp): rules + * below PN_REDUCE_SPLIT_1 live in ParserRunnerActions1.cpp (reduceRange1), + * below PN_REDUCE_SPLIT_2 in ParserRunnerActions2.cpp (reduceRange2), the + * rest in ParserRunnerActions3.cpp (reduceRange3). */ + +#ifndef PHPSTANTURBO_PN_ACTIONS_SPLIT_H +#define PHPSTANTURBO_PN_ACTIONS_SPLIT_H + +#define PN_REDUCE_SPLIT_1 269 +#define PN_REDUCE_SPLIT_2 454 + +#endif diff --git a/turbo-ext/src/parser/ParserRunnerHelpers.cpp b/turbo-ext/src/parser/ParserRunnerHelpers.cpp new file mode 100644 index 00000000000..0b86307be5c --- /dev/null +++ b/turbo-ext/src/parser/ParserRunnerHelpers.cpp @@ -0,0 +1,1902 @@ +/* + * ParserRunnerHelpers.cpp — native ports of php-parser 5.8.0's semantic-action + * helper methods (ParserAbstract.php lines 560-1341) plus the Node-side + * fromString builders they rely on (Int_/String_/Float_::fromString, + * Name::prepareName, Modifiers::verify*), as phpstanturbo::ParserEngine + * methods named after their ParserAbstract counterparts. + * + * Byte-identical AST output vs the PHP implementation is the acceptance bar: + * error messages, emitError order, and attribute insertion order are ported + * statement by statement. Ownership rules are documented in ParserEngine.h. + * + * Constants replicated from the php-parser sources (values verified there): + * Scalar\Int_: KIND_BIN=2, KIND_OCT=8, KIND_DEC=10, KIND_HEX=16 + * Scalar\String_: KIND_SINGLE_QUOTED=1, KIND_DOUBLE_QUOTED=2, + * KIND_HEREDOC=3, KIND_NOWDOC=4 + * Expr\Cast\Double: KIND_DOUBLE=1, KIND_FLOAT=2, KIND_REAL=3 + * Expr\Cast\Int_: KIND_INT=1, KIND_INTEGER=2 + * Expr\Cast\Bool_: KIND_BOOL=1, KIND_BOOLEAN=2 + * Expr\Cast\String_: KIND_STRING=1, KIND_BINARY=2 + * Expr\List_: KIND_ARRAY=2 + * Expr\Exit_: KIND_EXIT=1, KIND_DIE=2 + */ + +#include "ParserEngine.h" + +namespace phpstanturbo { + +/* ===== small utilities (pure string/number plumbing, deliberately zend-level) ===== */ + +/* Owned duplicate of an array (deep-ish: zend_array_dup, values addref'd). */ +static zv::Arr dupArray(zv::Ref arr) +{ + zv::Arr r; + ZVAL_ARR(r.raw(), zend_array_dup(Z_ARRVAL_P(arr.raw()))); + return r; +} + +static inline char toLowerAscii(char c) +{ + return (c >= 'A' && c <= 'Z') ? (char) (c + ('a' - 'A')) : c; +} + +/* Case-insensitive ASCII equality against a lowercase literal. */ +static bool iequals(zend_string *s, const char *lit, size_t litLen) +{ + if (ZSTR_LEN(s) != litLen) { + return false; + } + const char *v = ZSTR_VAL(s); + for (size_t i = 0; i < litLen; i++) { + if (toLowerAscii(v[i]) != lit[i]) { + return false; + } + } + return true; +} + +/* strpos(strtolower($hay), $needleLower) !== false */ +static bool containsLower(zend_string *hay, const char *needle, size_t nlen) +{ + const char *h = ZSTR_VAL(hay); + size_t hlen = ZSTR_LEN(hay); + if (nlen > hlen) { + return false; + } + for (size_t i = 0; i + nlen <= hlen; i++) { + size_t j = 0; + while (j < nlen && toLowerAscii(h[i + j]) == needle[j]) { + j++; + } + if (j == nlen) { + return true; + } + } + return false; +} + +static bool isHexDigit(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +/* borrowed "name" string prop of an Identifier/Name node, or NULL */ +static zend_string *nodeNameString(zv::Ref node) +{ + zv::Ref n = zv::ObjRef(node.raw()).prop("name", sizeof("name") - 1); + if (n.raw() == NULL || !n.isString()) { + return NULL; + } + return n.asString(); +} + +static bool isSpecialClassName(zend_string *name) +{ + return iequals(name, "self", 4) || iequals(name, "parent", 6) || iequals(name, "static", 6); +} + +/* ===== runtime token id constants (userland T_* values, cached per process) ===== */ + +#define PNH_TOK_UNRESOLVED (-3) +#define PNH_TOK_MISSING (-2) + +static zend_long g_tCommentId = PNH_TOK_UNRESOLVED; +static zend_long g_tDocCommentId = PNH_TOK_UNRESOLVED; +static zend_long g_tInlineHtmlId = PNH_TOK_UNRESOLVED; + +static zend_long tokenConstant(const char *name, size_t len, zend_long *cache) +{ + if (*cache == PNH_TOK_UNRESOLVED) { + zval *c = zend_get_constant_str(name, len); + *cache = (c != NULL && Z_TYPE_P(c) == IS_LONG) ? Z_LVAL_P(c) : PNH_TOK_MISSING; + } + return *cache; +} + +/* isset($this->dropTokens[$token->id]) — bound by dropTokensSize: + * T_BAD_CHARACTER sits above the grammar's symbol map */ +static bool isDropToken(const Tables *tables, int id) +{ + return id >= 0 && id < tables->dropTokensSize && tables->dropTokens[id]; +} + +/* ===== numeric parsing helpers ===== */ + +struct ParsedNum +{ + bool isDouble; + zend_long lval; + double dval; +}; + +/* + * Port of php-src _php_math_basetozval() (hexdec/bindec/octdec): invalid + * characters are skipped, values that overflow zend_long continue in double. + */ +static ParsedNum baseToNum(const char *s, size_t len, int base) +{ + ParsedNum r; + r.isDouble = false; + r.lval = 0; + r.dval = 0.0; + + zend_long num = 0; + double fnum = 0.0; + int mode = 0; + const zend_long cutoff = ZEND_LONG_MAX / base; + const int cutlim = (int) (ZEND_LONG_MAX % base); + + for (size_t i = 0; i < len; i++) { + char ch = s[i]; + int c; + if (ch >= '0' && ch <= '9') { + c = ch - '0'; + } else if (ch >= 'A' && ch <= 'Z') { + c = ch - 'A' + 10; + } else if (ch >= 'a' && ch <= 'z') { + c = ch - 'a' + 10; + } else { + continue; + } + if (c >= base) { + continue; + } + if (mode == 0) { + if (num < cutoff || (num == cutoff && c <= cutlim)) { + num = num * base + c; + continue; + } + fnum = (double) num; + mode = 1; + } + fnum = fnum * base + c; + } + + if (mode == 1) { + r.isDouble = true; + r.dval = fnum; + } else { + r.lval = num; + } + return r; +} + +/* + * strtol-style parse with saturation on overflow — matches PHP's (int) cast + * on integer-format strings and intval($str, $base). Only bases <= 10 are + * needed here (digits '0'-'9'). + */ +static zend_long strtolBase(const char *s, size_t len, int base) +{ + size_t i = 0; + while (i < len && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' + || s[i] == '\v' || s[i] == '\f' || s[i] == '\r')) { + i++; + } + bool neg = false; + if (i < len && (s[i] == '+' || s[i] == '-')) { + neg = s[i] == '-'; + i++; + } + zend_ulong acc = 0; + bool over = false; + const zend_ulong limit = neg ? ((zend_ulong) ZEND_LONG_MAX + 1) : (zend_ulong) ZEND_LONG_MAX; + for (; i < len; i++) { + char ch = s[i]; + if (ch < '0' || ch > '9') { + break; + } + zend_ulong d = (zend_ulong) (ch - '0'); + if ((int) d >= base) { + break; + } + if (!over) { + if (acc > (limit - d) / (zend_ulong) base) { + over = true; + } else { + acc = acc * (zend_ulong) base + d; + } + } + } + if (over) { + return neg ? ZEND_LONG_MIN : ZEND_LONG_MAX; + } + if (neg) { + return (zend_long) (0 - acc); + } + return (zend_long) acc; +} + +/* ===== string transformation helpers ===== */ + +/* str_replace($str, '_', '') — returns owned string */ +static zend_string *stripUnderscores(zend_string *in) +{ + if (memchr(ZSTR_VAL(in), '_', ZSTR_LEN(in)) == NULL) { + return zend_string_copy(in); + } + smart_str out = {}; + const char *s = ZSTR_VAL(in); + size_t n = ZSTR_LEN(in); + for (size_t i = 0; i < n; i++) { + if (s[i] != '_') { + smart_str_appendc(&out, s[i]); + } + } + return smart_str_extract(&out); +} + +/* One left-to-right non-overlapping pass replacing the 2-byte sequence f0 f1 with repl. */ +static zend_string *replace2Bytes(zend_string *in, char f0, char f1, char repl) +{ + smart_str out = {}; + const char *s = ZSTR_VAL(in); + size_t n = ZSTR_LEN(in); + size_t i = 0; + while (i < n) { + if (i + 1 < n && s[i] == f0 && s[i + 1] == f1) { + smart_str_appendc(&out, repl); + i += 2; + } else { + smart_str_appendc(&out, s[i]); + i++; + } + } + return smart_str_extract(&out); +} + +/* preg_replace('~(\r\n|\n|\r)\z~', '', $s) — consumes the input string */ +static zend_string *stripTrailingNewline(zend_string *s) +{ + const char *v = ZSTR_VAL(s); + size_t n = ZSTR_LEN(s); + size_t cut = 0; + if (n >= 2 && v[n - 2] == '\r' && v[n - 1] == '\n') { + cut = 2; + } else if (n >= 1 && (v[n - 1] == '\n' || v[n - 1] == '\r')) { + cut = 1; + } + if (cut == 0) { + return s; + } + zend_string *r = zend_string_init(v, n - cut, 0); + zend_string_release(s); + return r; +} + +/* String_::codePointToUtf8() — caller guarantees num <= 0x1FFFFF */ +static void utf8Append(smart_str *out, zend_ulong num) +{ + if (num <= 0x7F) { + smart_str_appendc(out, (char) num); + } else if (num <= 0x7FF) { + smart_str_appendc(out, (char) ((num >> 6) + 0xC0)); + smart_str_appendc(out, (char) ((num & 0x3F) + 0x80)); + } else if (num <= 0xFFFF) { + smart_str_appendc(out, (char) ((num >> 12) + 0xE0)); + smart_str_appendc(out, (char) (((num >> 6) & 0x3F) + 0x80)); + smart_str_appendc(out, (char) ((num & 0x3F) + 0x80)); + } else { + smart_str_appendc(out, (char) ((num >> 18) + 0xF0)); + smart_str_appendc(out, (char) (((num >> 12) & 0x3F) + 0x80)); + smart_str_appendc(out, (char) (((num >> 6) & 0x3F) + 0x80)); + smart_str_appendc(out, (char) ((num & 0x3F) + 0x80)); + } +} + +/* + * String_::parseEscapeSequences() port. Returns an owned string, or NULL after + * fatalError() (codepoint too large — the Error propagates out of the action + * in PHP; the engine aborts after the action returns). + */ +zend_string *ParserEngine::parseEscapeSequences(zend_string *strIn, bool hasQuote, char quote, bool parseUnicodeEscape) +{ + zend_string *str; + if (hasQuote) { + str = replace2Bytes(strIn, '\\', quote, quote); + } else { + str = zend_string_copy(strIn); + } + + const char *s = ZSTR_VAL(str); + size_t n = ZSTR_LEN(str); + smart_str out = {}; + size_t i = 0; + while (i < n) { + char c = s[i]; + if (c != '\\' || i + 1 >= n) { + smart_str_appendc(&out, c); + i++; + continue; + } + char d = s[i + 1]; + switch (d) { + case '\\': + case '$': + smart_str_appendc(&out, d); + i += 2; + continue; + case 'n': + smart_str_appendc(&out, '\n'); + i += 2; + continue; + case 'r': + smart_str_appendc(&out, '\r'); + i += 2; + continue; + case 't': + smart_str_appendc(&out, '\t'); + i += 2; + continue; + case 'f': + smart_str_appendc(&out, '\f'); + i += 2; + continue; + case 'v': + smart_str_appendc(&out, '\v'); + i += 2; + continue; + case 'e': + smart_str_appendc(&out, '\x1B'); + i += 2; + continue; + case 'x': + case 'X': { + size_t j = 0; + while (j < 2 && i + 2 + j < n && isHexDigit(s[i + 2 + j])) { + j++; + } + if (j == 0) { + break; /* no match: literal backslash */ + } + unsigned val = 0; + for (size_t t = 0; t < j; t++) { + char h = s[i + 2 + t]; + unsigned dv; + if (h >= '0' && h <= '9') { + dv = (unsigned) (h - '0'); + } else if (h >= 'a' && h <= 'f') { + dv = (unsigned) (h - 'a' + 10); + } else { + dv = (unsigned) (h - 'A' + 10); + } + val = val * 16 + dv; + } + smart_str_appendc(&out, (char) (val & 255)); + i += 2 + j; + continue; + } + case 'u': { + if (!parseUnicodeEscape) { + break; + } + if (i + 2 >= n || s[i + 2] != '{') { + break; + } + size_t k = i + 3; + size_t digits = 0; + while (k < n && isHexDigit(s[k])) { + k++; + digits++; + } + if (digits == 0 || k >= n || s[k] != '}') { + break; + } + ParsedNum cp = baseToNum(s + i + 3, digits, 16); + /* hexdec overflow → PHP_INT_MAX → codePointToUtf8 throws; > 0x1FFFFF throws */ + if (cp.isDouble || cp.lval > 0x1FFFFF) { + smart_str_free(&out); + zend_string_release(str); + fatalError("Invalid UTF-8 codepoint escape sequence: Codepoint too large", zv::Arr::empty()); + return NULL; + } + utf8Append(&out, (zend_ulong) cp.lval); + i = k + 1; + continue; + } + default: + if (d >= '0' && d <= '7') { + size_t j = 1; + while (j < 3 && i + 1 + j < n && s[i + 1 + j] >= '0' && s[i + 1 + j] <= '7') { + j++; + } + unsigned val = 0; + for (size_t t = 0; t < j; t++) { + val = val * 8 + (unsigned) (s[i + 1 + t] - '0'); + } + smart_str_appendc(&out, (char) (val & 255)); + i += 1 + j; + continue; + } + break; + } + /* no escape sequence matched at this backslash: emit it literally */ + smart_str_appendc(&out, '\\'); + i += 1; + } + zend_string_release(str); + return smart_str_extract(&out); +} + +/* + * String_::parseEscapeSequences($part->value, $quote, $unicode) through the + * real PHP static method (resolved via the class registry); writes the result + * back into the InterpolatedStringPart. Used by the encapsed-string actions. + */ +void ParserEngine::parseEscapeSequencesInPart(zv::Ref partNode, const char *quote) +{ + NodeClassInfo *cls = resolveNodeClass("Scalar\\String_", true); + if (cls == NULL || cls->ce == NULL) { + return; + } + zend_function *fn = (zend_function *) zend_hash_str_find_ptr( + &cls->ce->function_table, "parseescapesequences", sizeof("parseescapesequences") - 1); + if (fn == NULL) { + return; + } + zv::Ref value = prop(partNode, "value"); + if (value.raw() == NULL) { + return; + } + zval args[3]; + ZVAL_COPY(&args[0], value.raw()); + ZVAL_STRING(&args[1], quote); + ZVAL_BOOL(&args[2], phpVersionId >= 70000); /* PhpVersion::supportsUnicodeEscapes() */ + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_function(fn, NULL, cls->ce, &retval, 3, args, NULL); + zval_ptr_dtor(&args[1]); + zval_ptr_dtor(&args[0]); + if (UNEXPECTED(EG(exception) != NULL)) { + /* parseEscapeSequences throws PhpParser\Error for oversized \u{…} + * codepoints; doParse's catch (Error $e) must see it as an abort, + * not a raw exception escaping mid-reduce */ + zval_ptr_dtor(&retval); + abortForPendingException(); + return; + } + if (Z_TYPE(retval) == IS_UNDEF) { + return; + } + propWrite(partNode, "value", zv::Val::adopt(retval)); +} + +/* + * ParserAbstract::stripIndentation() port. The PHP implementation is a + * preg_replace_callback over /$start([ \t]*)($end)?/ where $start matches at + * line starts ((?<=\n), plus \A when $newlineAtStart) and $end is an + * empty-width group ((?=[\r\n]), plus \z when $newlineAtEnd). The callback + * emits errors (mixed indentation / insufficient level) and strips up to + * $indentLen chars of $indentChar from each line start. Note the regex also + * produces an empty match at the end of a string that ends with "\n" — the + * error checks run there too. + * + * Returns an owned string. Errors go through emitError with a copy of + * attrsBorrowed (stripIndentation emits, it never throws). + */ +zend_string *ParserEngine::stripIndentation(zend_string *str, zend_long indentLen, char indentChar, bool newlineAtStart, bool newlineAtEnd, zv::Ref attrsBorrowed) +{ + if (indentLen == 0) { + return zend_string_copy(str); + } + + const char *s = ZSTR_VAL(str); + size_t n = ZSTR_LEN(str); + const char other = indentChar == ' ' ? '\t' : ' '; + smart_str out = {}; + size_t pos = 0; + bool atLineStart = newlineAtStart; + + for (;;) { + if (atLineStart) { + size_t q = pos; + while (q < n && (s[q] == ' ' || s[q] == '\t')) { + q++; + } + size_t wsLen = q - pos; + size_t prefixLen = wsLen < (size_t) indentLen ? wsLen : (size_t) indentLen; + bool endGroupMatched = q < n ? (s[q] == '\r' || s[q] == '\n') : newlineAtEnd; + if (prefixLen > 0 && memchr(s + pos, other, prefixLen) != NULL) { + emitError("Invalid indentation - tabs and spaces cannot be mixed", zv::Val::copyOf(attrsBorrowed)); + } else if (prefixLen < (size_t) indentLen && !endGroupMatched) { + char msg[96]; + snprintf(msg, sizeof(msg), + "Invalid body indentation level (expecting an indentation level of at least " ZEND_LONG_FMT ")", + indentLen); + emitError(msg, zv::Val::copyOf(attrsBorrowed)); + } + if (q > pos + prefixLen) { + smart_str_appendl(&out, s + pos + prefixLen, q - (pos + prefixLen)); + } + pos = q; + } + if (pos >= n) { + break; + } + const char *nl = (const char *) memchr(s + pos, '\n', n - pos); + if (nl == NULL) { + smart_str_appendl(&out, s + pos, n - pos); + break; + } + size_t after = (size_t) (nl - s) + 1; + smart_str_appendl(&out, s + pos, after - pos); + pos = after; + atLineStart = true; + } + + return smart_str_extract(&out); +} + +/* ===== namespace handling ===== */ + +enum +{ + PNH_NS_NONE = 0, + PNH_NS_SEMICOLON = 1, + PNH_NS_BRACE = 2, +}; + +/* preg_match('/\A#!.*\r?\n\z/', $stmt->value): starts with "#!", ends with + * "\n", and the only "\n" is the final byte (`.` matches \r but not \n). */ +static bool isHashbangInlineHtml(zv::Ref stmt) +{ + zv::Ref value = zv::ObjRef(stmt.raw()).prop("value", sizeof("value") - 1); + if (value.raw() == NULL || !value.isString()) { + return false; + } + const char *s = Z_STRVAL_P(value.raw()); + size_t n = Z_STRLEN_P(value.raw()); + if (n < 3 || s[0] != '#' || s[1] != '!' || s[n - 1] != '\n') { + return false; + } + return memchr(s, '\n', n - 1) == NULL; +} + +/* getNamespaceErrorAttributes(): attrs copy with end* narrowed to the "namespace" keyword */ +zv::Arr ParserEngine::getNamespaceErrorAttributes(zv::Ref nsNode) +{ + zv::Arr attrs = getNodeAttributes(nsNode); + attrs.separate(); + HashTable *ht = attrs.table(); + zval *v; + + v = zend_hash_str_find(ht, "startLine", sizeof("startLine") - 1); + if (v != NULL && Z_TYPE_P(v) != IS_NULL) { + zval c; + ZVAL_COPY(&c, v); + zend_hash_str_update(ht, "endLine", sizeof("endLine") - 1, &c); + } + v = zend_hash_str_find(ht, "startTokenPos", sizeof("startTokenPos") - 1); + if (v != NULL && Z_TYPE_P(v) != IS_NULL) { + zval c; + ZVAL_COPY(&c, v); + zend_hash_str_update(ht, "endTokenPos", sizeof("endTokenPos") - 1, &c); + } + v = zend_hash_str_find(ht, "startFilePos", sizeof("startFilePos") - 1); + if (v != NULL && Z_TYPE_P(v) != IS_NULL) { + zval c; + ZVAL_LONG(&c, zval_get_long(v) + (zend_long) (sizeof("namespace") - 1) - 1); + zend_hash_str_update(ht, "endFilePos", sizeof("endFilePos") - 1, &c); + } + return attrs; +} + +int ParserEngine::getNamespacingStyle(zv::Ref stmts) +{ + int style = PNH_NS_NONE; + bool hasNotAllowedStmts = false; + zend_long i = -1; + + for (auto entry : zv::ArrRef(stmts.raw())) { + zv::Ref stmt = entry.value(); + i++; + if (stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\Namespace_")) { + zv::Ref sub = prop(stmt, "stmts"); + int currentStyle = (sub.raw() != NULL && Z_TYPE_P(sub.raw()) == IS_NULL) ? PNH_NS_SEMICOLON : PNH_NS_BRACE; + if (style == PNH_NS_NONE) { + style = currentStyle; + if (hasNotAllowedStmts) { + emitError("Namespace declaration statement has to be the very first statement in the script", + getNamespaceErrorAttributes(stmt)); + } + } else if (style != currentStyle) { + emitError("Cannot mix bracketed namespace declarations with unbracketed namespace declarations", + getNamespaceErrorAttributes(stmt)); + /* Treat like semicolon style for namespace normalization */ + return PNH_NS_SEMICOLON; + } + continue; + } + + /* declare(), __halt_compiler() and nops can be used before a namespace declaration */ + if (stmt.isObject() + && (isInstanceOf(stmt, "Node\\Stmt\\Declare_") + || isInstanceOf(stmt, "Node\\Stmt\\HaltCompiler") + || isInstanceOf(stmt, "Node\\Stmt\\Nop"))) { + continue; + } + + /* There may be a hashbang line at the very start of the file */ + if (i == 0 && stmt.isObject() + && isInstanceOf(stmt, "Node\\Stmt\\InlineHTML") + && isHashbangInlineHtml(stmt)) { + continue; + } + + hasNotAllowedStmts = true; + } + + return style; +} + +/* fixupNamespaceAttributes(): extend the namespace node's end attributes to its last stmt */ +void ParserEngine::fixupNamespaceAttributes(zv::Ref nsNode) +{ + zv::Ref stmts = prop(nsNode, "stmts"); + if (stmts.raw() == NULL || !stmts.isArray()) { + return; + } + uint32_t count = zend_hash_num_elements(stmts.asArrayTable()); + if (count == 0) { + return; + } + zv::Ref lastStmt = itemAt(stmts, count - 1); + if (lastStmt.raw() == NULL || !lastStmt.isObject()) { + return; + } + zv::Arr lastAttrs = getNodeAttributes(lastStmt); + static const char *const endKeys[3] = {"endLine", "endFilePos", "endTokenPos"}; + for (int k = 0; k < 3; k++) { + /* hasAttribute() is array_key_exists: null values count as present */ + zval *v = zend_hash_str_find(lastAttrs.table(), endKeys[k], strlen(endKeys[k])); + if (v != NULL) { + setNodeAttribute(nsNode, endKeys[k], zv::Val::copyOf(zv::Ref(v))); + } + } +} + +zv::Val ParserEngine::handleNamespaces(zv::Ref stmts) +{ + int style = getNamespacingStyle(stmts); + + if (style == PNH_NS_NONE) { + /* not namespaced, nothing to do */ + return zv::Val::copyOf(stmts); + } + + if (style == PNH_NS_BRACE) { + /* only check that there are no invalid statements between the namespaces */ + bool afterFirstNamespace = false; + bool hasErrored = false; + for (auto entry : zv::ArrRef(stmts.raw())) { + zv::Ref stmt = entry.value(); + if (stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\Namespace_")) { + afterFirstNamespace = true; + } else if (!(stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\HaltCompiler")) + && !(stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\Nop")) + && afterFirstNamespace && !hasErrored) { + emitError("No code may exist outside of namespace {}", getNodeAttributes(stmt)); + hasErrored = true; /* Avoid one error for every statement */ + } + } + return zv::Val::copyOf(stmts); + } + + /* For semicolon namespaces move the statements after a namespace declaration into ->stmts */ + zv::Arr resultStmts = zv::Arr::empty(); + zval *lastNs = NULL; + zval *pendingNs = NULL; /* the namespace the pending stmts belong to, if any */ + zv::Arr pendingStmts; /* UNDEF while no semicolon namespace is open */ + + /* close out the last semicolon-style namespace: write collected stmts, fix attributes + * (the deferred equivalent of PHP's `$stmt->stmts = []` + by-ref appends) */ + auto closePendingNamespace = [&]() { + if (pendingNs != NULL) { + propWrite(zv::Ref(pendingNs), "stmts", std::move(pendingStmts)); + pendingNs = NULL; + } + fixupNamespaceAttributes(zv::Ref(lastNs)); + }; + + for (auto entry : zv::ArrRef(stmts.raw())) { + zv::Ref stmt = entry.value(); + if (stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\Namespace_")) { + if (lastNs != NULL) { + closePendingNamespace(); + } + zv::Ref sub = prop(stmt, "stmts"); + if (sub.raw() != NULL && Z_TYPE_P(sub.raw()) == IS_NULL) { + pendingNs = stmt.raw(); + pendingStmts = zv::Arr::empty(); + resultStmts.push(stmt); + } else { + /* This handles the invalid case of mixed style namespaces */ + resultStmts.push(stmt); + pendingNs = NULL; + } + lastNs = stmt.raw(); + } else if (stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\HaltCompiler")) { + /* __halt_compiler() is not moved into the namespace */ + resultStmts.push(stmt); + } else { + if (pendingNs != NULL) { + pendingStmts.push(stmt); + } else { + resultStmts.push(stmt); + } + } + } + + if (lastNs != NULL) { + closePendingNamespace(); + } + return resultStmts; +} + +/* ===== handleBuiltinTypes ===== */ + +static const struct +{ + const char *name; + uint32_t len; + int minVersion; +} g_builtinTypes[] = { + {"array", 5, 50100}, + {"callable", 8, 50400}, + {"bool", 4, 70000}, + {"int", 3, 70000}, + {"float", 5, 70000}, + {"string", 6, 70000}, + {"iterable", 8, 70100}, + {"void", 4, 70100}, + {"object", 6, 70200}, + {"null", 4, 80000}, + {"false", 5, 80000}, + {"mixed", 5, 80000}, + {"never", 5, 80100}, + {"true", 4, 80200}, +}; + +zv::Val ParserEngine::handleBuiltinTypes(zv::Ref nameNode) +{ + /* Name::isUnqualified() is overridden to return false in FullyQualified/Relative */ + if (isInstanceOf(nameNode, "Node\\Name\\FullyQualified") + || isInstanceOf(nameNode, "Node\\Name\\Relative")) { + return zv::Val::copyOf(nameNode); + } + zv::Ref nameProp = prop(nameNode, "name"); + if (nameProp.raw() == NULL || !nameProp.isString() + || memchr(Z_STRVAL_P(nameProp.raw()), '\\', Z_STRLEN_P(nameProp.raw())) != NULL) { + return zv::Val::copyOf(nameNode); + } + + zend_string *lower = zend_string_tolower(nameProp.asString()); + int minVersion = -1; + for (size_t i = 0; i < sizeof(g_builtinTypes) / sizeof(g_builtinTypes[0]); i++) { + if (ZSTR_LEN(lower) == g_builtinTypes[i].len + && memcmp(ZSTR_VAL(lower), g_builtinTypes[i].name, g_builtinTypes[i].len) == 0) { + minVersion = g_builtinTypes[i].minVersion; + break; + } + } + if (minVersion < 0 || phpVersionId < (zend_long) minVersion) { + zend_string_release(lower); + return zv::Val::copyOf(nameNode); + } + + return newNode("Node\\Identifier", getNodeAttributes(nameNode), zv::Val::adoptString(lower)); +} + +/* ===== cast kind helpers ===== */ + +zend_long ParserEngine::getFloatCastKind(zv::Ref castTokenText) +{ + zend_string *s = castTokenText.asString(); + if (containsLower(s, "float", 5)) { + return 2; /* Double::KIND_FLOAT */ + } + if (containsLower(s, "real", 4)) { + return 3; /* Double::KIND_REAL */ + } + return 1; /* Double::KIND_DOUBLE */ +} + +zend_long ParserEngine::getIntCastKind(zv::Ref castTokenText) +{ + if (containsLower(castTokenText.asString(), "integer", 7)) { + return 2; /* Cast\Int_::KIND_INTEGER */ + } + return 1; /* Cast\Int_::KIND_INT */ +} + +zend_long ParserEngine::getBoolCastKind(zv::Ref castTokenText) +{ + if (containsLower(castTokenText.asString(), "boolean", 7)) { + return 2; /* Cast\Bool_::KIND_BOOLEAN */ + } + return 1; /* Cast\Bool_::KIND_BOOL */ +} + +zend_long ParserEngine::getStringCastKind(zv::Ref castTokenText) +{ + if (containsLower(castTokenText.asString(), "binary", 6)) { + return 2; /* Cast\String_::KIND_BINARY */ + } + return 1; /* Cast\String_::KIND_STRING */ +} + +/* ===== parseLNumber (Int_::fromString + the emit-and-dummy catch) ===== */ + +zv::Val ParserEngine::parseLNumber(zv::Ref str, zv::Arr attributes, bool allowInvalidOctal) +{ + zend_string *orig = str.asString(); + + /* Int_::fromString mutates a by-value copy of $attributes; the catch in + * parseLNumber constructs the dummy node with the ORIGINAL attributes + * (without rawValue/kind). Keep both. */ + zv::Arr workAttrs = dupArray(attributes.ref()); + workAttrs.set("rawValue", zv::Val::string(orig)); + + zend_string *stripped = stripUnderscores(orig); + const char *s = ZSTR_VAL(stripped); + size_t n = ZSTR_LEN(stripped); + + zend_long kind; + zend_long value; + + if (n == 0 || s[0] != '0' || n == 1) { + kind = 10; /* Int_::KIND_DEC */ + value = strtolBase(s, n, 10); /* (int) cast: saturating for pure digit strings */ + } else if (s[1] == 'x' || s[1] == 'X') { + kind = 16; /* Int_::KIND_HEX */ + ParsedNum num = baseToNum(s, n, 16); + /* overflow to double would TypeError in PHP; unreachable via the lexer */ + value = num.isDouble ? zend_dval_to_lval(num.dval) : num.lval; + } else if (s[1] == 'b' || s[1] == 'B') { + kind = 2; /* Int_::KIND_BIN */ + ParsedNum num = baseToNum(s, n, 2); + value = num.isDouble ? zend_dval_to_lval(num.dval) : num.lval; + } else { + if (!allowInvalidOctal + && (memchr(s, '8', n) != NULL || memchr(s, '9', n) != NULL)) { + zend_string_release(stripped); + /* throw new Error('Invalid numeric literal', $attributes) — caught + * by parseLNumber: emitError($error); return new Int_(0, $attributes) */ + emitError("Invalid numeric literal", std::move(workAttrs)); + return newNode("Node\\Scalar\\Int_", std::move(attributes), zv::Val::integer(0)); + } + /* Strip optional explicit octal prefix. */ + size_t skip = (s[1] == 'o' || s[1] == 'O') ? 2 : 0; + kind = 8; /* Int_::KIND_OCT */ + /* intval($str, 8): strtol semantics, cuts at the first invalid digit */ + value = strtolBase(s + skip, n - skip, 8); + } + + zend_string_release(stripped); + attributes.release(); + workAttrs.set("kind", zv::Val::integer(kind)); + return newNode("Node\\Scalar\\Int_", std::move(workAttrs), zv::Val::integer(value)); +} + +/* ===== parseNumString ===== */ + +zv::Val ParserEngine::parseNumString(zv::Ref str, zv::Val attributes) +{ + zend_string *zstr = str.asString(); + const char *s = ZSTR_VAL(zstr); + size_t n = ZSTR_LEN(zstr); + + /* /^(?:0|-?[1-9][0-9]*)$/ — PCRE '$' also matches before one final "\n" */ + size_t core = (n > 0 && s[n - 1] == '\n') ? n - 1 : n; + bool matches = false; + if (core == 1 && s[0] == '0') { + matches = true; + } else { + size_t i = 0; + if (i < core && s[i] == '-') { + i++; + } + if (i < core && s[i] >= '1' && s[i] <= '9') { + i++; + while (i < core && s[i] >= '0' && s[i] <= '9') { + i++; + } + matches = i == core; + } + } + + if (matches) { + /* $num = +$str; is_int($num) — overflow to float means String_ */ + bool neg = s[0] == '-'; + size_t i = neg ? 1 : 0; + zend_ulong acc = 0; + bool over = false; + const zend_ulong limit = neg ? ((zend_ulong) ZEND_LONG_MAX + 1) : (zend_ulong) ZEND_LONG_MAX; + for (; i < core; i++) { + zend_ulong d = (zend_ulong) (s[i] - '0'); + if (acc > (limit - d) / 10) { + over = true; + break; + } + acc = acc * 10 + d; + } + if (!over) { + zend_long value = neg ? (zend_long) (0 - acc) : (zend_long) acc; + return newNode("Node\\Scalar\\Int_", std::move(attributes), zv::Val::integer(value)); + } + } + + return newNode("Node\\Scalar\\String_", std::move(attributes), zv::Val::string(zstr)); +} + +/* ===== parseDocString ===== */ + +/* label from /\A[bB]?<<<[ \t]*['"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/ */ +static bool isLabelChar(unsigned char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c == '_' || c >= 0x7f; +} + +static zend_string *docLabel(zend_string *startToken) +{ + const char *s = ZSTR_VAL(startToken); + size_t n = ZSTR_LEN(startToken); + size_t i = 0; + if (i < n && (s[i] == 'b' || s[i] == 'B')) { + i++; + } + if (i + 3 <= n && memcmp(s + i, "<<<", 3) == 0) { + i += 3; + } + while (i < n && (s[i] == ' ' || s[i] == '\t')) { + i++; + } + if (i < n && (s[i] == '\'' || s[i] == '"')) { + i++; + } + size_t start = i; + while (i < n && isLabelChar((unsigned char) s[i])) { + i++; + } + return zend_string_init(s + start, i - start, 0); +} + +zv::Val ParserEngine::parseDocString(zv::Ref startTokenRef, zv::Ref contents, zv::Ref endTokenRef, zv::Arr attributes, zv::Val endAttributes, bool parseUnicodeEscape) +{ + zend_string *startToken = startTokenRef.asString(); + zend_string *endToken = endTokenRef.asString(); + + zend_long kind = memchr(ZSTR_VAL(startToken), '\'', ZSTR_LEN(startToken)) == NULL + ? 3 /* String_::KIND_HEREDOC */ + : 4 /* String_::KIND_NOWDOC */; + + zend_string *label = docLabel(startToken); + + /* /\A[ \t]*\/ of the end token */ + size_t indLen = 0; + { + const char *e = ZSTR_VAL(endToken); + size_t en = ZSTR_LEN(endToken); + while (indLen < en && (e[indLen] == ' ' || e[indLen] == '\t')) { + indLen++; + } + } + zend_string *indentation = zend_string_init(ZSTR_VAL(endToken), indLen, 0); + + attributes.set("kind", zv::Val::integer(kind)); + attributes.set("docLabel", zv::Val::adoptString(label)); /* transfers the label reference */ + attributes.set("docIndentation", zv::Val::string(indentation)); + + bool indentHasSpaces = memchr(ZSTR_VAL(indentation), ' ', ZSTR_LEN(indentation)) != NULL; + bool indentHasTabs = memchr(ZSTR_VAL(indentation), '\t', ZSTR_LEN(indentation)) != NULL; + zend_long indentLen; + if (indentHasSpaces && indentHasTabs) { + emitError("Invalid indentation - tabs and spaces cannot be mixed", std::move(endAttributes)); + /* Proceed processing as if this doc string is not indented */ + indentLen = 0; + } else { + endAttributes.release(); + indentLen = (zend_long) ZSTR_LEN(indentation); + } + /* computed from the pre-reset flags, exactly like the PHP source */ + char indentChar = indentHasSpaces ? ' ' : '\t'; + zend_string_release(indentation); + + if (contents.isString()) { + zend_string *contentsStr = contents.asString(); + if (ZSTR_LEN(contentsStr) == 0) { + attributes.set("rawValue", zv::Val::string(contentsStr)); + return newNode("Node\\Scalar\\String_", std::move(attributes), zv::Val::string("", 0)); + } + + zend_string *stripped = stripIndentation(contentsStr, indentLen, indentChar, true, true, attributes.ref()); + stripped = stripTrailingNewline(stripped); + attributes.set("rawValue", zv::Val::string(stripped)); + + zend_string *value; + if (kind == 3 /* KIND_HEREDOC */) { + value = parseEscapeSequences(stripped, false, 0, parseUnicodeEscape); + zend_string_release(stripped); + if (value == NULL) { + return zv::Val(); + } + } else { + value = stripped; + } + return newNode("Node\\Scalar\\String_", std::move(attributes), zv::Val::adoptString(value)); + } + + /* interpolated: array of (Expr|InterpolatedStringPart) */ + zv::ArrRef parts(contents.raw()); + uint32_t numParts = parts.size(); + + zv::Ref first = itemAt(contents, 0); + if (first.raw() != NULL && first.isObject() + && !isInstanceOf(first, "Node\\InterpolatedStringPart")) { + /* If there is no leading encapsed string part, pretend there is an + * empty one — called purely for the error side effects. */ + zv::Arr firstAttrs = getNodeAttributes(first); + zend_string *empty = ZSTR_EMPTY_ALLOC(); + zend_string *r = stripIndentation(empty, indentLen, indentChar, true, false, firstAttrs.ref()); + zend_string_release(r); + zend_string_release(empty); + } + + zv::Arr newContents = zv::Arr::empty(); + zend_long i = -1; + for (auto entry : parts) { + zv::Ref part = entry.value(); + i++; + if (part.isObject() && isInstanceOf(part, "Node\\InterpolatedStringPart")) { + bool isLast = (uint32_t) (i + 1) == numParts; + zv::Ref valueProp = prop(part, "value"); + if (valueProp.raw() == NULL || !valueProp.isString()) { + newContents.push(part); + continue; + } + zv::Arr partAttrs = getNodeAttributes(part); + zend_string *stripped = stripIndentation(valueProp.asString(), + indentLen, indentChar, i == 0, isLast, partAttrs.ref()); + partAttrs.release(); + if (isLast) { + stripped = stripTrailingNewline(stripped); + } + propWrite(part, "value", zv::Val::string(stripped)); + setNodeAttribute(part, "rawValue", zv::Val::string(stripped)); + zend_string *parsed = parseEscapeSequences(stripped, false, 0, parseUnicodeEscape); + zend_string_release(stripped); + if (parsed == NULL) { + return zv::Val(); + } + bool isEmpty = ZSTR_LEN(parsed) == 0; + propWrite(part, "value", zv::Val::adoptString(parsed)); + if (isEmpty) { + continue; + } + } + newContents.push(part); + } + + return newNode("Node\\Scalar\\InterpolatedString", std::move(attributes), newContents); +} + +/* ===== comment-driven Nop creation ===== */ + +/* + * getCommentBeforeToken(): returns the token index of the last comment before + * $tokenPos (walking back over drop tokens), or -1. The PHP code materializes + * a Comment object here; its only observable use in the Nop helpers is the + * end line/pos/tokenPos, which we compute directly from the token. + */ +int ParserEngine::getCommentBeforeToken(int tokenPos) +{ + zend_long tComment = tokenConstant("T_COMMENT", sizeof("T_COMMENT") - 1, &g_tCommentId); + zend_long tDocComment = tokenConstant("T_DOC_COMMENT", sizeof("T_DOC_COMMENT") - 1, &g_tDocCommentId); + while (--tokenPos >= 0) { + const Token *t = &tokens[tokenPos]; + if (!isDropToken(tables, t->id)) { + break; + } + if ((zend_long) t->id == tComment || (zend_long) t->id == tDocComment) { + return tokenPos; + } + } + return -1; +} + +zv::Val ParserEngine::maybeCreateZeroLengthNop(int tokenPos) +{ + int ci = getCommentBeforeToken(tokenPos); + if (ci < 0) { + return zv::Val::null(); + } + const Token *t = &tokens[ci]; + const char *text = ZSTR_VAL(t->text); + size_t tlen = ZSTR_LEN(t->text); + + /* Comment::getEndLine() = $token->line + substr_count($token->text, "\n") */ + zend_long nlCount = 0; + { + const char *p = text; + const char *e = text + tlen; + while (p < e && (p = (const char *) memchr(p, '\n', (size_t) (e - p))) != NULL) { + nlCount++; + p++; + } + } + zend_long commentEndLine = (zend_long) t->line + nlCount; + /* Comment::getEndFilePos() = $token->getEndPos() - 1 = pos + strlen(text) - 1 */ + zend_long commentEndFilePos = (zend_long) t->pos + (zend_long) tlen - 1; + zend_long commentEndTokenPos = (zend_long) ci; + + zv::Arr attrs = zv::Arr::create(6); + attrs.set("startLine", zv::Val::integer(commentEndLine)); + attrs.set("endLine", zv::Val::integer(commentEndLine)); + attrs.set("startFilePos", zv::Val::integer(commentEndFilePos + 1)); + attrs.set("endFilePos", zv::Val::integer(commentEndFilePos)); + attrs.set("startTokenPos", zv::Val::integer(commentEndTokenPos + 1)); + attrs.set("endTokenPos", zv::Val::integer(commentEndTokenPos)); + return newNode("Node\\Stmt\\Nop", std::move(attrs)); +} + +zv::Val ParserEngine::maybeCreateNop(int tokenStartPos, int tokenEndPos) +{ + if (getCommentBeforeToken(tokenStartPos) < 0) { + return zv::Val::null(); + } + return newNode("Node\\Stmt\\Nop", getAttributes(tokenStartPos, tokenEndPos)); +} + +/* ===== handleHaltCompiler / inlineHtmlHasLeadingNewline ===== */ + +zv::Val ParserEngine::handleHaltCompiler() +{ + zend_string *text = NULL; + int next = tokenPos + 1; + if (next >= 0 && next < numTokens) { + const Token *t = &tokens[next]; + zend_long tInlineHtml = tokenConstant("T_INLINE_HTML", sizeof("T_INLINE_HTML") - 1, &g_tInlineHtmlId); + if ((zend_long) t->id == tInlineHtml) { + text = t->text; + } + } + /* Prevent the lexer from returning any further tokens. */ + tokenPos = numTokens - 2; + + if (text != NULL) { + return zv::Val::string(text); + } + return zv::Val::string("", 0); +} + +bool ParserEngine::inlineHtmlHasLeadingNewline(int stackPos) +{ + int pos = tokenStartStack[stackPos]; + if (pos > 0) { + zend_string *prevText = tokens[pos - 1].text; + return memchr(ZSTR_VAL(prevText), '\n', ZSTR_LEN(prevText)) != NULL + || memchr(ZSTR_VAL(prevText), '\r', ZSTR_LEN(prevText)) != NULL; + } + return true; +} + +/* ===== array destructuring fixups ===== */ + +zv::Val ParserEngine::fixupArrayDestructuring(zv::Ref arrayNode) +{ + createdArraysRemove(arrayNode); + + zv::Arr newItems = zv::Arr::empty(); + zv::Ref items = prop(arrayNode, "items"); + if (items.raw() != NULL && items.isArray()) { + for (auto entry : zv::ArrRef(items.raw())) { + zv::Ref item = entry.value(); + if (!item.isObject()) { + newItems.push(item); + continue; + } + zv::Ref value = prop(item, "value"); + if (value.raw() != NULL && value.isObject() + && isInstanceOf(value, "Node\\Expr\\Error")) { + /* Error was a placeholder for an empty element, legal in destructuring */ + newItems.push(zv::Val::null()); + continue; + } + if (value.raw() != NULL && value.isObject() + && isInstanceOf(value, "Node\\Expr\\Array_")) { + zv::Val inner = fixupArrayDestructuring(value); + if (aborted) { + return zv::Val(); + } + zv::Ref key = prop(item, "key"); + if (key.raw() != NULL && Z_TYPE_P(key.raw()) == IS_NULL) { + key = zv::Ref(NULL); + } + zv::Ref byRef = prop(item, "byRef"); + /* new ArrayItem($fixedUp, $item->key, $item->byRef, $item->getAttributes()) */ + zv::Val newItem = newNode("Node\\ArrayItem", getNodeAttributes(item), + inner, key.raw() != NULL ? Borrowed(key) : Borrowed(nullptr), byRef, zv::Val::boolean(false)); + if (aborted) { + return zv::Val(); + } + newItems.push(std::move(newItem)); + continue; + } + newItems.push(item); + } + } + + /* ['kind' => Expr\List_::KIND_ARRAY] + $node->getAttributes(): + * left keys first, right-side keys appended unless already present */ + zv::Arr attrs = zv::Arr::create(6); + attrs.set("kind", zv::Val::integer(2 /* Expr\List_::KIND_ARRAY */)); + { + zv::Arr nodeAttrs = getNodeAttributes(arrayNode); + zend_ulong idx; + zend_string *k; + zval *v; + ZEND_HASH_FOREACH_KEY_VAL(nodeAttrs.table(), idx, k, v) { + if (k != NULL) { + if (!zend_hash_exists(attrs.table(), k)) { + Z_TRY_ADDREF_P(v); + zend_hash_add_new(attrs.table(), k, v); + } + } else { + if (!zend_hash_index_exists(attrs.table(), idx)) { + Z_TRY_ADDREF_P(v); + zend_hash_index_add_new(attrs.table(), idx, v); + } + } + } ZEND_HASH_FOREACH_END(); + } + + return newNode("Node\\Expr\\List_", std::move(attrs), newItems); +} + +void ParserEngine::postprocessList(zv::Ref listNode) +{ + zv::Ref items = prop(listNode, "items"); + if (items.raw() == NULL || !items.isArray()) { + return; + } + + bool any = false; + for (auto entry : zv::ArrRef(items.raw())) { + zv::Ref item = entry.value(); + if (!item.isObject()) { + continue; + } + zv::Ref value = prop(item, "value"); + if (value.raw() != NULL && value.isObject() + && isInstanceOf(value, "Node\\Expr\\Error")) { + any = true; + break; + } + } + if (!any) { + return; + } + + /* $node->items[$i] = null for the Error placeholders */ + zv::Arr newItems = dupArray(items); + zend_ulong idx; + zval *it; + ZEND_HASH_FOREACH_NUM_KEY_VAL(newItems.table(), idx, it) { + if (Z_TYPE_P(it) != IS_OBJECT) { + continue; + } + zv::Ref value = prop(zv::Ref(it), "value"); + if (value.raw() != NULL && value.isObject() + && isInstanceOf(value, "Node\\Expr\\Error")) { + zval nz; + ZVAL_NULL(&nz); + zend_hash_index_update(newItems.table(), idx, &nz); + } + } ZEND_HASH_FOREACH_END(); + propWrite(listNode, "items", std::move(newItems)); +} + +/* ===== fixupAlternativeElse ===== */ + +void ParserEngine::fixupAlternativeElse(zv::Ref node) +{ + /* Make sure a trailing nop statement carrying comments is part of the node. */ + zv::Ref stmts = prop(node, "stmts"); + if (stmts.raw() == NULL || !stmts.isArray()) { + return; + } + uint32_t numStmts = zend_hash_num_elements(stmts.asArrayTable()); + if (numStmts == 0) { + return; + } + zv::Ref last = itemAt(stmts, numStmts - 1); + if (last.raw() == NULL || !last.isObject() + || !isInstanceOf(last, "Node\\Stmt\\Nop")) { + return; + } + zv::Arr nopAttrs = getNodeAttributes(last); + static const char *const endKeys[3] = {"endLine", "endFilePos", "endTokenPos"}; + for (int k = 0; k < 3; k++) { + /* the PHP code uses isset() here, so null values do NOT count */ + zval *v = zend_hash_str_find(nopAttrs.table(), endKeys[k], strlen(endKeys[k])); + if (v != NULL && Z_TYPE_P(v) != IS_NULL) { + setNodeAttribute(node, endKeys[k], zv::Val::copyOf(zv::Ref(v))); + } + } +} + +/* ===== modifier verification (Modifiers::verify*) ===== */ + +static const char *modifierToString(zend_long modifier) +{ + switch (modifier) { + case PN_MOD_PUBLIC: + return "public"; + case PN_MOD_PROTECTED: + return "protected"; + case PN_MOD_PRIVATE: + return "private"; + case PN_MOD_STATIC: + return "static"; + case PN_MOD_ABSTRACT: + return "abstract"; + case PN_MOD_FINAL: + return "final"; + case PN_MOD_READONLY: + return "readonly"; + case PN_MOD_PUBLIC_SET: + return "public(set)"; + case PN_MOD_PROTECTED_SET: + return "protected(set)"; + case PN_MOD_PRIVATE_SET: + return "private(set)"; + } + return "unknown"; /* Modifiers::toString would throw; unreachable from the grammar */ +} + +void ParserEngine::checkClassModifier(zend_long a, zend_long b, int modifierStackPos) +{ + /* Modifiers::verifyClassModifier throws at the FIRST violation only */ + if ((a & b) != 0) { + zend_string *msg = zend_strpprintf(0, "Multiple %s modifiers are not allowed", modifierToString(b)); + emitError(msg, getAttributesAt(modifierStackPos)); + zend_string_release(msg); + return; + } + if ((a & 48) != 0 && (b & 48) != 0) { + emitError("Cannot use the final modifier on an abstract class", getAttributesAt(modifierStackPos)); + } +} + +void ParserEngine::verifyModifier(zend_long a, zend_long b, int modifierStackPos) +{ + const zend_long visibilityMask = PN_MOD_PUBLIC | PN_MOD_PROTECTED | PN_MOD_PRIVATE; + const zend_long visibilitySetMask = PN_MOD_PUBLIC_SET | PN_MOD_PROTECTED_SET | PN_MOD_PRIVATE_SET; + + if (((a & visibilityMask) != 0 && (b & visibilityMask) != 0) + || ((a & visibilitySetMask) != 0 && (b & visibilitySetMask) != 0)) { + emitError("Multiple access type modifiers are not allowed", getAttributesAt(modifierStackPos)); + return; + } + if ((a & b) != 0) { + zend_string *msg = zend_strpprintf(0, "Multiple %s modifiers are not allowed", modifierToString(b)); + emitError(msg, getAttributesAt(modifierStackPos)); + zend_string_release(msg); + return; + } + if ((a & 48) != 0 && (b & 48) != 0) { + emitError("Cannot use the final modifier on an abstract class member", getAttributesAt(modifierStackPos)); + } +} + +void ParserEngine::checkModifier(zend_long a, zend_long b, int modifierStackPos) +{ + verifyModifier(a, b, modifierStackPos); +} + +void ParserEngine::checkPropertyHookModifiers(zend_long a, zend_long b, int modifierPos) +{ + verifyModifier(a, b, modifierPos); + /* checked independently — both errors can be emitted from one call */ + if (b != PN_MOD_FINAL) { + zend_string *msg = zend_strpprintf(0, "Cannot use the %s modifier on a property hook", modifierToString(b)); + emitError(msg, getAttributesAt(modifierPos)); + zend_string_release(msg); + } +} + +/* ===== structural check* helpers ===== */ + +void ParserEngine::checkParam(zv::Ref param) +{ + zv::Ref variadic = prop(param, "variadic"); + zv::Ref def = prop(param, "default"); + if (variadic.raw() != NULL && variadic.isTrue() + && def.raw() != NULL && def.isObject()) { + emitError("Variadic parameter cannot have a default value", getNodeAttributes(def)); + } + + zv::Ref type = prop(param, "type"); + if (type.raw() != NULL && type.isObject() + && isInstanceOf(type, "Node\\Identifier")) { + zv::Ref tn = prop(type, "name"); + if (tn.raw() != NULL && tn.stringEquals("void")) { + emitError("void cannot be used as a parameter type", getNodeAttributes(type)); + } + } +} + +void ParserEngine::checkTryCatch(zv::Ref node) +{ + zv::Ref catches = prop(node, "catches"); + zv::Ref finally = prop(node, "finally"); + bool noCatches = catches.raw() == NULL || !catches.isArray() + || zend_hash_num_elements(catches.asArrayTable()) == 0; + if (noCatches && finally.raw() != NULL && Z_TYPE_P(finally.raw()) == IS_NULL) { + emitError("Cannot use try without catch or finally", getNodeAttributes(node)); + } +} + +void ParserEngine::checkNamespace(zv::Ref node) +{ + zv::Ref stmts = prop(node, "stmts"); + if (stmts.raw() == NULL || !stmts.isArray()) { + return; + } + for (auto entry : zv::ArrRef(stmts.raw())) { + zv::Ref stmt = entry.value(); + if (stmt.isObject() && isInstanceOf(stmt, "Node\\Stmt\\Namespace_")) { + emitError("Namespace declarations cannot be nested", getNodeAttributes(stmt)); + } + } +} + +void ParserEngine::checkClassName(zv::Ref name, int namePos) +{ + if (name.raw() == NULL || !name.isObject()) { + return; + } + zend_string *n = nodeNameString(name); + if (n == NULL || !isSpecialClassName(n)) { + return; + } + zend_string *msg = zend_strpprintf(0, "Cannot use '%s' as class name as it is reserved", ZSTR_VAL(n)); + emitError(msg, getAttributesAt(namePos)); + zend_string_release(msg); +} + +void ParserEngine::checkImplementedInterfaces(zv::Ref interfaces) +{ + if (interfaces.raw() == NULL || !interfaces.isArray()) { + return; + } + for (auto entry : zv::ArrRef(interfaces.raw())) { + zv::Ref iface = entry.value(); + if (!iface.isObject()) { + continue; + } + zend_string *n = nodeNameString(iface); + if (n == NULL || !isSpecialClassName(n)) { + continue; + } + zend_string *msg = zend_strpprintf(0, "Cannot use '%s' as interface name as it is reserved", ZSTR_VAL(n)); + emitError(msg, getNodeAttributes(iface)); + zend_string_release(msg); + } +} + +void ParserEngine::checkClass(zv::Ref node, int namePos) +{ + checkClassName(prop(node, "name"), namePos); + + zv::Ref extends = prop(node, "extends"); + if (extends.raw() != NULL && extends.isObject()) { + zend_string *n = nodeNameString(extends); + if (n != NULL && isSpecialClassName(n)) { + zend_string *msg = zend_strpprintf(0, "Cannot use '%s' as class name as it is reserved", ZSTR_VAL(n)); + emitError(msg, getNodeAttributes(extends)); + zend_string_release(msg); + } + } + + checkImplementedInterfaces(prop(node, "implements")); +} + +void ParserEngine::checkInterface(zv::Ref node, int namePos) +{ + checkClassName(prop(node, "name"), namePos); + checkImplementedInterfaces(prop(node, "extends")); +} + +void ParserEngine::checkEnum(zv::Ref node, int namePos) +{ + checkClassName(prop(node, "name"), namePos); + checkImplementedInterfaces(prop(node, "implements")); +} + +void ParserEngine::checkClassMethod(zv::Ref node, int modifierPos) +{ + zv::Ref flags = prop(node, "flags"); + zend_long f = flags.raw() != NULL ? flags.toLong() : 0; + zv::Ref name = prop(node, "name"); + zend_string *n = (name.raw() != NULL && name.isObject()) ? nodeNameString(name) : NULL; + if (n == NULL) { + return; + } + + if ((f & PN_MOD_STATIC) != 0) { + const char *fmt = NULL; + if (iequals(n, "__construct", sizeof("__construct") - 1)) { + fmt = "Constructor %s() cannot be static"; + } else if (iequals(n, "__destruct", sizeof("__destruct") - 1)) { + fmt = "Destructor %s() cannot be static"; + } else if (iequals(n, "__clone", sizeof("__clone") - 1)) { + fmt = "Clone method %s() cannot be static"; + } + if (fmt != NULL) { + zend_string *msg = zend_strpprintf(0, fmt, ZSTR_VAL(n)); + emitError(msg, getAttributesAt(modifierPos)); + zend_string_release(msg); + } + } + + if ((f & PN_MOD_READONLY) != 0) { + zend_string *msg = zend_strpprintf(0, "Method %s() cannot be readonly", ZSTR_VAL(n)); + emitError(msg, getAttributesAt(modifierPos)); + zend_string_release(msg); + } +} + +void ParserEngine::checkClassConst(zv::Ref node, int modifierPos) +{ + zv::Ref flags = prop(node, "flags"); + zend_long f = flags.raw() != NULL ? flags.toLong() : 0; + static const zend_long modifiers[3] = {PN_MOD_STATIC, PN_MOD_ABSTRACT, PN_MOD_READONLY}; + for (int i = 0; i < 3; i++) { + if ((f & modifiers[i]) != 0) { + zend_string *msg = zend_strpprintf(0, "Cannot use '%s' as constant modifier", + modifierToString(modifiers[i])); + emitError(msg, getAttributesAt(modifierPos)); + zend_string_release(msg); + } + } +} + +void ParserEngine::checkUseUse(zv::Ref node, int namePos) +{ + zv::Ref alias = prop(node, "alias"); + if (alias.raw() == NULL || !alias.isObject()) { + return; + } + zend_string *aliasStr = nodeNameString(alias); + if (aliasStr == NULL || !isSpecialClassName(aliasStr)) { + return; + } + zv::Ref name = prop(node, "name"); + zend_string *nameStr = (name.raw() != NULL && name.isObject()) ? nodeNameString(name) : NULL; + /* sprintf('Cannot use %s as %s because \'%2$s\' is a special class name', ...) */ + zend_string *msg = zend_strpprintf(0, "Cannot use %s as %s because '%s' is a special class name", + nameStr != NULL ? ZSTR_VAL(nameStr) : "", ZSTR_VAL(aliasStr), ZSTR_VAL(aliasStr)); + emitError(msg, getAttributesAt(namePos)); + zend_string_release(msg); +} + +void ParserEngine::checkPropertyHooksForMultiProperty(zv::Ref property, int hookPos) +{ + zv::Ref props = prop(property, "props"); + if (props.raw() != NULL && props.isArray() + && zend_hash_num_elements(props.asArrayTable()) > 1) { + emitError("Cannot use hooks when declaring multiple properties", getAttributesAt(hookPos)); + } +} + +void ParserEngine::checkEmptyPropertyHookList(zv::Ref hooks, int hookPos) +{ + if (hooks.raw() == NULL || !hooks.isArray() + || zend_hash_num_elements(hooks.asArrayTable()) == 0) { + emitError("Property hook list cannot be empty", getAttributesAt(hookPos)); + } +} + +void ParserEngine::checkPropertyHook(zv::Ref hook, int paramListPos, bool hasParamList) +{ + zv::Ref name = prop(hook, "name"); + if (name.raw() == NULL || !name.isObject()) { + return; + } + zend_string *n = nodeNameString(name); + if (n == NULL) { + return; + } + bool isGet = iequals(n, "get", 3); + bool isSet = iequals(n, "set", 3); + if (!isGet && !isSet) { + zend_string *msg = zend_strpprintf(0, "Unknown hook \"%s\", expected \"get\" or \"set\"", ZSTR_VAL(n)); + emitError(msg, getNodeAttributes(name)); + zend_string_release(msg); + } + if (isGet && hasParamList) { + emitError("get hook must not have a parameter list", getAttributesAt(paramListPos)); + } +} + +void ParserEngine::checkConstantAttributes(zv::Ref node) +{ + zv::Ref attrGroups = prop(node, "attrGroups"); + zv::Ref consts = prop(node, "consts"); + bool hasAttrGroups = attrGroups.raw() != NULL && attrGroups.isArray() + && zend_hash_num_elements(attrGroups.asArrayTable()) != 0; + bool multipleConsts = consts.raw() != NULL && consts.isArray() + && zend_hash_num_elements(consts.asArrayTable()) > 1; + if (hasAttrGroups && multipleConsts) { + emitError("Cannot use attributes on multiple constants at once", getNodeAttributes(node)); + } +} + +void ParserEngine::checkPipeOperatorParentheses(zv::Ref expr) +{ + if (!expr.isObject()) { + return; + } + if (!isInstanceOf(expr, "Node\\Expr\\ArrowFunction")) { + return; + } + if (zend_hash_index_exists(&parenthesizedArrowFns, (zend_ulong) Z_OBJ_HANDLE_P(expr.raw()))) { + return; + } + emitError("Arrow functions on the right hand side of |> must be parenthesized", getNodeAttributes(expr)); +} + +void ParserEngine::addPropertyNameToHooks(zv::Ref node) +{ + zv::Val nameVal; + + if (isInstanceOf(node, "Node\\Stmt\\Property")) { + /* $node->props[0]->name->toString() */ + zv::Ref props = prop(node, "props"); + zv::Ref first = (props.raw() != NULL && props.isArray()) + ? itemAt(props, 0) + : zv::Ref(NULL); + if (first.raw() != NULL && first.isObject()) { + zv::Ref ident = prop(first, "name"); + if (ident.raw() != NULL && ident.isObject()) { + zv::Ref n = prop(ident, "name"); + if (n.raw() != NULL) { + nameVal = zv::Val::copyOf(n); + } + } + } + } else { + /* Param: $node->var->name */ + zv::Ref var = prop(node, "var"); + if (var.raw() != NULL && var.isObject()) { + zv::Ref n = prop(var, "name"); + if (n.raw() != NULL) { + nameVal = zv::Val::copyOf(n); + } + } + } + if (nameVal.isUndef()) { + return; + } + + zv::Ref hooks = prop(node, "hooks"); + if (hooks.raw() != NULL && hooks.isArray()) { + for (auto entry : zv::ArrRef(hooks.raw())) { + zv::Ref hook = entry.value(); + if (!hook.isObject()) { + continue; + } + setNodeAttribute(hook, "propertyName", zv::Val::copyOf(nameVal.ref())); + } + } +} + +/* ===== createExitExpr ===== */ + +zv::Val ParserEngine::createExitExpr(zv::Ref nameStr, int namePos, zv::Ref args, zv::Arr attributes) +{ + HashTable *argsHt = (args.raw() != NULL && args.isArray()) ? args.asArrayTable() : NULL; + uint32_t numArgs = argsHt != NULL ? zend_hash_num_elements(argsHt) : 0; + + /* isSimpleExit() */ + bool simple = false; + zv::Ref firstArg(NULL); + if (numArgs == 0) { + simple = true; + } else if (numArgs == 1) { + firstArg = itemAt(args, 0); + if (firstArg.raw() != NULL && firstArg.isObject() + && isInstanceOf(firstArg, "Node\\Arg")) { + zv::Ref argName = prop(firstArg, "name"); + zv::Ref byRef = prop(firstArg, "byRef"); + zv::Ref unpack = prop(firstArg, "unpack"); + simple = argName.raw() != NULL && Z_TYPE_P(argName.raw()) == IS_NULL + && byRef.raw() != NULL && byRef.isFalse() + && unpack.raw() != NULL && unpack.isFalse(); + } + } + + if (simple) { + /* Create Exit node for backwards compatibility. */ + zend_long kind = iequals(nameStr.asString(), "exit", 4) + ? 1 /* Exit_::KIND_EXIT */ + : 2 /* Exit_::KIND_DIE */; + attributes.set("kind", zv::Val::integer(kind)); + zv::Ref exprVal(NULL); + if (numArgs == 1 && firstArg.raw() != NULL) { + exprVal = prop(firstArg, "value"); + if (exprVal.raw() != NULL && Z_TYPE_P(exprVal.raw()) == IS_NULL) { + exprVal = zv::Ref(NULL); + } + } + return newNode("Node\\Expr\\Exit_", std::move(attributes), + exprVal.raw() != NULL ? Borrowed(exprVal) : Borrowed(nullptr)); + } + + zv::Val nameNode = newName(nameStr, getAttributesAt(namePos)); + if (aborted) { + return zv::Val(); + } + return newNode("Node\\Expr\\FuncCall", std::move(attributes), nameNode, args); +} + +/* ===== Name builders (Name::prepareName) ===== */ + +/* Returns an owned prepared name string or NULL after fatalError. */ +zend_string *ParserEngine::prepareName(zv::Ref nameVal) +{ + if (nameVal.isString()) { + if (ZSTR_LEN(nameVal.asString()) == 0) { + /* InvalidArgumentException in PHP; mapped onto the abort channel */ + fatalError("Name cannot be empty", zv::Arr::empty()); + return NULL; + } + return zend_string_copy(nameVal.asString()); + } + if (nameVal.isArray()) { + if (zend_hash_num_elements(nameVal.asArrayTable()) == 0) { + fatalError("Name cannot be empty", zv::Arr::empty()); + return NULL; + } + smart_str out = {}; + bool isFirst = true; + for (auto entry : zv::ArrRef(nameVal.raw())) { + if (!isFirst) { + smart_str_appendc(&out, '\\'); + } + zend_string *ps = zval_get_string(entry.value().raw()); + smart_str_append(&out, ps); + zend_string_release(ps); + isFirst = false; + } + return smart_str_extract(&out); + } + if (nameVal.isObject() && isInstanceOf(nameVal, "Node\\Name")) { + zv::Ref inner = prop(nameVal, "name"); + if (inner.raw() != NULL && inner.isString()) { + return zend_string_copy(inner.asString()); + } + } + fatalError("Expected string, array of parts or Name instance", zv::Arr::empty()); + return NULL; +} + +zv::Val ParserEngine::newNameVariant(const char *alias, zv::Ref strOrParts, zv::Val attributes) +{ + zend_string *prepared = prepareName(strOrParts); + if (prepared == NULL) { + return zv::Val(); + } + /* Name's final ctor only runs prepareName + assigns; constructing with the + * already-prepared string through the prop-slot path is byte-equivalent. */ + return newNode(alias, std::move(attributes), zv::Val::adoptString(prepared)); +} + +zv::Val ParserEngine::newName(zv::Ref strOrParts, zv::Val attributes) +{ + return newNameVariant("Node\\Name", strOrParts, std::move(attributes)); +} + +/* ===== String_::fromString ===== */ + +zv::Val ParserEngine::stringFromString(zv::Ref raw, zv::Arr attributes, bool parseUnicodeEscape) +{ + zend_string *rawStr = raw.asString(); + const char *s = ZSTR_VAL(rawStr); + size_t n = ZSTR_LEN(rawStr); + + bool singleQuoted = (n > 0 && s[0] == '\'') + || (n > 1 && s[1] == '\'' && (s[0] == 'b' || s[0] == 'B')); + attributes.set("kind", zv::Val::integer(singleQuoted ? 1 /* KIND_SINGLE_QUOTED */ : 2 /* KIND_DOUBLE_QUOTED */)); + attributes.set("rawValue", zv::Val::string(rawStr)); + + /* String_::parse() */ + size_t bLength = (n > 0 && (s[0] == 'b' || s[0] == 'B')) ? 1 : 0; + size_t innerLen = n >= bLength + 2 ? n - bLength - 2 : 0; + zend_string *inner = zend_string_init(s + bLength + 1, innerLen, 0); + + zend_string *value; + if (bLength < n && s[bLength] == '\'') { + /* str_replace(['\\\\', '\\\''], ['\\', '\''], ...): two sequential passes */ + zend_string *pass1 = replace2Bytes(inner, '\\', '\\', '\\'); + value = replace2Bytes(pass1, '\\', '\'', '\''); + zend_string_release(pass1); + } else { + value = parseEscapeSequences(inner, true, '"', parseUnicodeEscape); + } + zend_string_release(inner); + if (value == NULL) { + return zv::Val(); + } + + return newNode("Node\\Scalar\\String_", std::move(attributes), zv::Val::adoptString(value)); +} + +/* ===== Float_::fromString ===== */ + +zv::Val ParserEngine::floatFromString(zv::Ref raw, zv::Arr attributes) +{ + zend_string *orig = raw.asString(); + attributes.set("rawValue", zv::Val::string(orig)); + + zend_string *stripped = stripUnderscores(orig); + const char *s = ZSTR_VAL(stripped); + size_t n = ZSTR_LEN(stripped); + + double value = 0.0; + bool handled = false; + if (n > 0 && s[0] == '0') { + if (n > 1 && (s[1] == 'x' || s[1] == 'X')) { + ParsedNum num = baseToNum(s, n, 16); + value = num.isDouble ? num.dval : (double) num.lval; + handled = true; + } else if (n > 1 && (s[1] == 'b' || s[1] == 'B')) { + ParsedNum num = baseToNum(s, n, 2); + value = num.isDouble ? num.dval : (double) num.lval; + handled = true; + } else if (memchr(s, '.', n) == NULL && memchr(s, 'e', n) == NULL + && memchr(s, 'E', n) == NULL) { + /* octdec(substr($str, 0, strcspn($str, '89'))) */ + size_t cut = 0; + while (cut < n && s[cut] != '8' && s[cut] != '9') { + cut++; + } + ParsedNum num = baseToNum(s, cut, 8); + value = num.isDouble ? num.dval : (double) num.lval; + handled = true; + } + } + if (!handled) { + /* (float) cast on the DNUMBER text */ + value = zend_strtod(s, NULL); + } + zend_string_release(stripped); + + zval v; + ZVAL_DOUBLE(&v, value); + return newNode("Node\\Scalar\\Float_", std::move(attributes), zv::Val::adopt(v)); +} + +} // namespace phpstanturbo diff --git a/turbo-ext/src/parser/action-overrides/0ffe3fa3e57871da42f4e4d382a75a3380e24f77.inc b/turbo-ext/src/parser/action-overrides/0ffe3fa3e57871da42f4e4d382a75a3380e24f77.inc new file mode 100644 index 00000000000..25672aec175 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/0ffe3fa3e57871da42f4e4d382a75a3380e24f77.inc @@ -0,0 +1,8 @@ + /* Error node created now to remember the position; later either reported or + * converted to a null element, depending on creation vs destructuring context. + * createEmptyElemAttributes() delegates to getAttributesForToken(); the SAME + * attrs array is shared between the Error node and the ArrayItem, as in PHP. */ + zv::Arr attrs = getAttributesForToken(tokenPos); + zv::Val attrsForError = zv::Val::copyOf(attrs.ref()); + zv::Val err = newNode("Expr\\Error", std::move(attrsForError)); + semValue = newNode("Node\\ArrayItem", std::move(attrs), err, nullptr, zv::Val::boolean(false), zv::Val::boolean(false)); diff --git a/turbo-ext/src/parser/action-overrides/302966ab17149baef35dbb007abfe742d2706046.inc b/turbo-ext/src/parser/action-overrides/302966ab17149baef35dbb007abfe742d2706046.inc new file mode 100644 index 00000000000..a4cac7833c6 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/302966ab17149baef35dbb007abfe742d2706046.inc @@ -0,0 +1,10 @@ + /* $attrs['kind'] = KIND_DOUBLE_QUOTED; double-quote parseEscapeSequences() on the + * InterpolatedStringParts of $2; $$ = new Scalar\InterpolatedString($2, $attrs) */ + zv::Arr attrs = PN_ATTRS(3, 1, 3); + attrs.set("kind", zv::Val::integer(2 /* Scalar\String_::KIND_DOUBLE_QUOTED */)); + for (auto entry : zv::ArrRef(PN_SEM(3, 2).raw())) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "\""); + } + } + semValue = newNode("Scalar\\InterpolatedString", std::move(attrs), PN_SEM(3, 2)); diff --git a/turbo-ext/src/parser/action-overrides/33cd4fd5bb9aa00cd570b40ef1921386414be6a8.inc b/turbo-ext/src/parser/action-overrides/33cd4fd5bb9aa00cd570b40ef1921386414be6a8.inc new file mode 100644 index 00000000000..3221371f117 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/33cd4fd5bb9aa00cd570b40ef1921386414be6a8.inc @@ -0,0 +1,8 @@ + /* $$ = array($1); foreach ($$ as $s): backtick parseEscapeSequences() on InterpolatedStringParts */ + zv::Arr arr = arrayOf(PN_SEM(1, 1)); + for (auto entry : arr.arrRef()) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "`"); + } + } + semValue = std::move(arr); diff --git a/turbo-ext/src/parser/action-overrides/4d582a24c4d2e51f11f292d174b7c3141cadb3e4.inc b/turbo-ext/src/parser/action-overrides/4d582a24c4d2e51f11f292d174b7c3141cadb3e4.inc new file mode 100644 index 00000000000..2ac3a9bbe22 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/4d582a24c4d2e51f11f292d174b7c3141cadb3e4.inc @@ -0,0 +1,5 @@ + /* $$ = $self->parseNumString('-' . $2, attributes) */ + zend_string *cat = zend_string_concat2("-", 1, + Z_STRVAL_P(PN_SEM(2, 2).raw()), Z_STRLEN_P(PN_SEM(2, 2).raw())); + zv::Val tmp = zv::Val::adoptString(cat); + semValue = parseNumString(tmp.ref(), PN_ATTRS(2, 1, 2)); diff --git a/turbo-ext/src/parser/action-overrides/4e1822bc7002c2a89dd7a63f0c78b90f91b21da1.inc b/turbo-ext/src/parser/action-overrides/4e1822bc7002c2a89dd7a63f0c78b90f91b21da1.inc new file mode 100644 index 00000000000..1e7f2627fef --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/4e1822bc7002c2a89dd7a63f0c78b90f91b21da1.inc @@ -0,0 +1,3 @@ + /* list($class, $ctorArgs) = $self->semStack[...]; $$ = new Expr\New_($class, $ctorArgs, attributes); */ + zv::Ref pair = PN_SEM(2, 2); + semValue = newNode("Expr\\New_", PN_ATTRS(2, 1, 2), itemAt(pair, 0), itemAt(pair, 1)); diff --git a/turbo-ext/src/parser/action-overrides/9010e0ff64f5ffbc6b330506e2e596585337c3f7.inc b/turbo-ext/src/parser/action-overrides/9010e0ff64f5ffbc6b330506e2e596585337c3f7.inc new file mode 100644 index 00000000000..e011e8e47ac --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/9010e0ff64f5ffbc6b330506e2e596585337c3f7.inc @@ -0,0 +1,9 @@ + /* $var = $1->name; $$ = \is_string($var) ? new VarLikeIdentifier($var, attributes) : $var; */ + zv::Ref var = prop(PN_SEM(1, 1), "name"); + if (var.raw() != NULL && var.isString()) { + semValue = newNode("Node\\VarLikeIdentifier", PN_ATTRS(1, 1, 1), var); + } else if (var.raw() != NULL) { + semValue = var; + } else { + semValue = zv::Val::null(); + } diff --git a/turbo-ext/src/parser/action-overrides/aeb46c35c63d49d076d22af3cd5ef3ffc214e9dc.inc b/turbo-ext/src/parser/action-overrides/aeb46c35c63d49d076d22af3cd5ef3ffc214e9dc.inc new file mode 100644 index 00000000000..79c1cdcd021 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/aeb46c35c63d49d076d22af3cd5ef3ffc214e9dc.inc @@ -0,0 +1,7 @@ + /* $nop = $self->maybeCreateZeroLengthNop($self->tokenPos); + * if ($nop !== null) { $self->semStack[$1][] = $nop; } $$ = $self->semStack[$1]; */ + zv::Val nop = maybeCreateZeroLengthNop(tokenPos); + if (!nop.isNull()) { + pushOnto(PN_SEM(1, 1), std::move(nop)); + } + semValue = PN_SEM(1, 1); diff --git a/turbo-ext/src/parser/action-overrides/c903abbc90c691e24a2e00c5eeed110ffb990a89.inc b/turbo-ext/src/parser/action-overrides/c903abbc90c691e24a2e00c5eeed110ffb990a89.inc new file mode 100644 index 00000000000..62523cf6101 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/c903abbc90c691e24a2e00c5eeed110ffb990a89.inc @@ -0,0 +1,12 @@ + /* $$ = $1; if ($$[count($$)-1]->value instanceof Expr\Error) array_pop($$); */ + semValue = PN_SEM(1, 1); + uint32_t n = zv::ArrRef(semValue.raw()).size(); + if (n > 0) { + zv::Ref last = itemAt(semValue.ref(), n - 1); + if (last.raw() != NULL) { + zv::Ref value = prop(last, "value"); + if (value.raw() != NULL && isInstanceOf(value, "Expr\\Error")) { + phpArrayPop(semValue.raw()); + } + } + } diff --git a/turbo-ext/src/parser/action-overrides/dd5881dec8adf82f4b92c400bfedca8c43ef66b4.inc b/turbo-ext/src/parser/action-overrides/dd5881dec8adf82f4b92c400bfedca8c43ef66b4.inc new file mode 100644 index 00000000000..21839b9cb23 --- /dev/null +++ b/turbo-ext/src/parser/action-overrides/dd5881dec8adf82f4b92c400bfedca8c43ef66b4.inc @@ -0,0 +1,7 @@ + /* foreach ($1 as $s): backtick parseEscapeSequences() on InterpolatedStringParts; $$ = $1 */ + for (auto entry : zv::ArrRef(PN_SEM(1, 1).raw())) { + if (isInstanceOf(entry.value(), "Node\\InterpolatedStringPart")) { + parseEscapeSequencesInPart(entry.value(), "`"); + } + } + semValue = PN_SEM(1, 1); diff --git a/turbo-ext/src/reg.h b/turbo-ext/src/reg.h new file mode 100644 index 00000000000..13839800b09 --- /dev/null +++ b/turbo-ext/src/reg.h @@ -0,0 +1,252 @@ +/* + * Fluent, zero-cost class registration — PHP-CPP's extension.add() look + * without its call-time cost. The builder assembles the same + * zend_internal_arg_info / zend_function_entry structures the PHP_METHOD + + * ZEND_BEGIN_ARG_INFO_EX + PHP_ME macro triple produced, and hands the + * engine the raw handler pointers directly — no trampoline, no Php::Value + * boxing, byte-identical dispatch. Handlers are plain functions or + * non-capturing lambdas with the (INTERNAL_FUNCTION_PARAMETERS) signature, + * so each method is declared exactly once: name, flags, signature and body + * together at the registration site. + * + * Everything here runs once at module startup; allocations are persistent + * (the engine references the arginfo arrays for the process lifetime). + */ + +#ifndef PHPSTANTURBO_REG_H +#define PHPSTANTURBO_REG_H + +#include "support.h" + +#include +#include + +namespace reg { + +constexpr uint32_t Public = ZEND_ACC_PUBLIC; +constexpr uint32_t Private = ZEND_ACC_PRIVATE; +constexpr uint32_t Static = ZEND_ACC_STATIC; +constexpr uint32_t PublicStatic = ZEND_ACC_PUBLIC | ZEND_ACC_STATIC; + +/* One parameter's metadata, mirroring what the ZEND_ARG_* macros encode. */ +struct Arg +{ + const char *name; + uint32_t typeMask; /* MAY_BE_* mask incl. by-ref/variadic bits */ + const char *className = nullptr; /* persistent literal for object types */ +}; + +namespace detail { + +inline uint32_t flagBits(bool byRef, bool variadic) +{ + return _ZEND_ARG_INFO_FLAGS(byRef ? ZEND_SEND_BY_REF : ZEND_SEND_BY_VAL, variadic ? 1 : 0, 0); +} + +inline uint32_t codeMask(zend_uchar code, bool nullable) +{ + uint32_t mask = code == _IS_BOOL ? MAY_BE_BOOL : (uint32_t) (1u << code); + return mask | (nullable ? MAY_BE_NULL : 0); +} + +} // namespace detail + +/* an untyped parameter (ZEND_ARG_INFO) */ +inline Arg any(const char *name, bool byRef = false) +{ + return { name, detail::flagBits(byRef, false), nullptr }; +} + +inline Arg longArg(const char *name) +{ + return { name, detail::codeMask(IS_LONG, false) | detail::flagBits(false, false), nullptr }; +} + +inline Arg boolArg(const char *name) +{ + return { name, detail::codeMask(_IS_BOOL, false) | detail::flagBits(false, false), nullptr }; +} + +inline Arg stringArg(const char *name) +{ + return { name, detail::codeMask(IS_STRING, false) | detail::flagBits(false, false), nullptr }; +} + +inline Arg arrayArg(const char *name) +{ + return { name, detail::codeMask(IS_ARRAY, false) | detail::flagBits(false, false), nullptr }; +} + +inline Arg callableArg(const char *name) +{ + return { name, MAY_BE_CALLABLE | detail::flagBits(false, false), nullptr }; +} + +inline Arg objectArg(const char *name, bool nullable = false) +{ + return { name, detail::codeMask(IS_OBJECT, nullable) | detail::flagBits(false, false), nullptr }; +} + +/* object of a specific class; className must be a persistent literal */ +inline Arg obj(const char *name, const char *className, bool nullable = false) +{ + return { name, _ZEND_TYPE_LITERAL_NAME_BIT | (nullable ? MAY_BE_NULL : 0) | detail::flagBits(false, false), className }; +} + +inline Arg variadicObj(const char *name, const char *className) +{ + return { name, _ZEND_TYPE_LITERAL_NAME_BIT | detail::flagBits(false, true), className }; +} + +/* + * Builder for one internal class. Usage: + * + * reg::Class cls("PHPStanTurbo\\Foo"); + * cls.privateLongProperty("value", 0); + * cls.method("bar", reg::Public, 1, { reg::longArg("x") }, + * [](INTERNAL_FUNCTION_PARAMETERS) { ... }); + * ce = cls.register_(); + */ +class Class +{ +public: + explicit Class(const char *name) : name(name) {} + + /* + * requiredArgs is ZEND_BEGIN_ARG_INFO_EX's required_num_args; args carry + * name/type/by-ref/variadic exactly as the ZEND_ARG_* macros would. + */ + Class &method(const char *methodName, uint32_t flags, uint32_t requiredArgs, std::initializer_list args, zif_handler handler) + { + /* arginfo array: slot 0 is the return-info slot carrying the + * required-args count, exactly as ZEND_BEGIN_ARG_INFO_EX emits */ + auto *argInfo = (zend_internal_arg_info *) pemalloc(sizeof(zend_internal_arg_info) * (args.size() + 1), 1); + argInfo[0].name = (const char *) (uintptr_t) requiredArgs; + argInfo[0].type.ptr = NULL; + argInfo[0].type.type_mask = 0; + argInfo[0].default_value = NULL; + size_t i = 1; + for (const Arg &arg : args) { + argInfo[i].name = arg.name; + argInfo[i].type.ptr = (void *) arg.className; + argInfo[i].type.type_mask = arg.typeMask; + argInfo[i].default_value = NULL; + i++; + } + + zend_function_entry entry; + memset(&entry, 0, sizeof(entry)); + entry.fname = methodName; + entry.handler = handler; + entry.arg_info = argInfo; + entry.num_args = (uint32_t) args.size(); + entry.flags = flags; + entries.push_back(entry); + return *this; + } + + /* declaration order defines the OBJ_PROP_NUM slot, as with the macros */ + Class &privateLongProperty(const char *propertyName, zend_long defaultValue) + { + properties.push_back({ propertyName, PropertyKind::Long, ZEND_ACC_PRIVATE, defaultValue }); + return *this; + } + + /* a private null-initialised property (zend_declare_property_null) */ + Class &privateNullProperty(const char *propertyName) + { + properties.push_back({ propertyName, PropertyKind::Null, ZEND_ACC_PRIVATE, 0 }); + return *this; + } + + /* a protected property defaulting to an empty array (zend_declare_property + * with ZVAL_EMPTY_ARRAY) */ + Class &protectedArrayProperty(const char *propertyName) + { + properties.push_back({ propertyName, PropertyKind::EmptyArray, ZEND_ACC_PROTECTED, 0 }); + return *this; + } + + /* a protected bool property (zend_declare_property_bool) */ + Class &protectedBoolProperty(const char *propertyName, bool defaultValue) + { + properties.push_back({ propertyName, PropertyKind::Bool, ZEND_ACC_PROTECTED, defaultValue ? 1 : 0 }); + return *this; + } + + /* a public long class constant (zend_declare_class_constant_long) */ + Class &classConstantLong(const char *constantName, zend_long value) + { + constants.push_back({ constantName, value }); + return *this; + } + + zend_class_entry *register_() + { + zend_function_entry sentinel; + memset(&sentinel, 0, sizeof(sentinel)); + entries.push_back(sentinel); + + /* the engine copies fentry contents but references arg_info forever; + * the entries vector lives only through this call, argInfo persists */ + zend_class_entry ce; + INIT_CLASS_ENTRY_EX(ce, name, strlen(name), entries.data()); + zend_class_entry *registered = zend_register_internal_class(&ce); + for (const Property &property : properties) { + size_t len = strlen(property.name); + switch (property.kind) { + case PropertyKind::Long: + zend_declare_property_long(registered, property.name, len, property.defaultValue, property.visibility); + break; + case PropertyKind::Null: + zend_declare_property_null(registered, property.name, len, property.visibility); + break; + case PropertyKind::Bool: + zend_declare_property_bool(registered, property.name, len, property.defaultValue, property.visibility); + break; + case PropertyKind::EmptyArray: { + zval emptyArray; + ZVAL_EMPTY_ARRAY(&emptyArray); + zend_declare_property(registered, property.name, len, &emptyArray, property.visibility); + break; + } + } + } + for (const Constant &constant : constants) { + zend_declare_class_constant_long(registered, constant.name, strlen(constant.name), constant.value); + } + return registered; + } + +private: + enum class PropertyKind + { + Long, + Null, + Bool, + EmptyArray, + }; + + struct Property + { + const char *name; + PropertyKind kind; + uint32_t visibility; + zend_long defaultValue; + }; + + struct Constant + { + const char *name; + zend_long value; + }; + + const char *name; + std::vector entries; + std::vector properties; + std::vector constants; +}; + +} // namespace reg + +#endif diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp new file mode 100644 index 00000000000..e74498f0fa1 --- /dev/null +++ b/turbo-ext/src/support.cpp @@ -0,0 +1,907 @@ +#include "support.h" + +#include + +pt_globals_t pt_globals; + +zend_class_entry *pt_ce_trinary = nullptr; +zend_class_entry *pt_ce_expr_type_holder = nullptr; +zend_class_entry *pt_ce_cond_expr_holder = nullptr; + +/* {{{ class map */ + +typedef struct _pt_class_template { + const char *key; + const char *default_name; +} pt_class_template; + +static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { + /* PT_CLASS_TYPE_COMBINATOR */ {"typeCombinator", "PHPStan\\Type\\TypeCombinator"}, + /* PT_CLASS_BOOLEAN_TYPE */ {"booleanType", "PHPStan\\Type\\BooleanType"}, + /* PT_CLASS_CONSTANT_BOOLEAN_TYPE */ {"constantBooleanType", "PHPStan\\Type\\Constant\\ConstantBooleanType"}, + /* PT_CLASS_SHOULD_NOT_HAPPEN */ {"shouldNotHappenException", "PHPStan\\ShouldNotHappenException"}, + /* PT_CLASS_VERBOSITY_LEVEL */ {"verbosityLevel", "PHPStan\\Type\\VerbosityLevel"}, + /* PT_CLASS_VARIABLE */ {"variable", "PhpParser\\Node\\Expr\\Variable"}, + /* PT_CLASS_FUNC_CALL */ {"funcCall", "PhpParser\\Node\\Expr\\FuncCall"}, + /* PT_CLASS_VIRTUAL_NODE */ {"virtualNode", "PHPStan\\Node\\VirtualNode"}, + /* PT_CLASS_NODE */ {"node", "PhpParser\\Node"}, + /* PT_CLASS_NAME */ {"name", "PhpParser\\Node\\Name"}, + /* PT_CLASS_EXPR */ {"expr", "PhpParser\\Node\\Expr"}, + /* PT_CLASS_PROPERTY_FETCH */ {"propertyFetch", "PhpParser\\Node\\Expr\\PropertyFetch"}, + /* PT_CLASS_INTERTWINED_VAR */ {"intertwinedVariableByReferenceWithExpr", "PHPStan\\Node\\Expr\\IntertwinedVariableByReferenceWithExpr"}, + /* PT_CLASS_ARRAY_DIM_FETCH */ {"arrayDimFetch", "PhpParser\\Node\\Expr\\ArrayDimFetch"}, + /* PT_CLASS_METHOD_CALL */ {"methodCall", "PhpParser\\Node\\Expr\\MethodCall"}, + /* PT_CLASS_FUNCTION_LIKE */ {"functionLike", "PhpParser\\Node\\FunctionLike"}, + /* PT_CLASS_CALL_LIKE */ {"callLike", "PhpParser\\Node\\Expr\\CallLike"}, + /* PT_CLASS_STATIC_CALL */ {"staticCall", "PhpParser\\Node\\Expr\\StaticCall"}, + /* PT_CLASS_NEW */ {"newExpr", "PhpParser\\Node\\Expr\\New_"}, + /* PT_CLASS_CLASS_STMT */ {"classStmt", "PhpParser\\Node\\Stmt\\Class_"}, + /* PT_CLASS_VARIADIC_PLACEHOLDER */ {"variadicPlaceholder", "PhpParser\\Node\\VariadicPlaceholder"}, + /* PT_CLASS_ERROR_TYPE */ {"errorType", "PHPStan\\Type\\ErrorType"}, + /* PT_CLASS_SCALAR */ {"scalar", "PhpParser\\Node\\Scalar"}, + /* PT_CLASS_ARRAY_EXPR */ {"arrayExpr", "PhpParser\\Node\\Expr\\Array_"}, + /* PT_CLASS_UNARY_MINUS */ {"unaryMinus", "PhpParser\\Node\\Expr\\UnaryMinus"}, + /* PT_CLASS_YIELD */ {"yield", "PhpParser\\Node\\Expr\\Yield_"}, + /* PT_CLASS_YIELD_FROM */ {"yieldFrom", "PhpParser\\Node\\Expr\\YieldFrom"}, + /* PT_CLASS_STMT */ {"stmt", "PhpParser\\Node\\Stmt"}, + /* PT_CLASS_NODE_VISITOR_ABSTRACT */ {"nodeVisitorAbstract", "PhpParser\\NodeVisitorAbstract"}, + /* PT_CLASS_CLOSURE_EXPR */ {"closureExpr", "PhpParser\\Node\\Expr\\Closure"}, + /* PT_CLASS_ARROW_FUNCTION */ {"arrowFunction", "PhpParser\\Node\\Expr\\ArrowFunction"}, + /* PT_CLASS_TYPE */ {"type", "PHPStan\\Type\\Type"}, + /* PT_CLASS_RECURSION_GUARD */ {"recursionGuard", "PHPStan\\Type\\RecursionGuard"}, + /* PT_CLASS_TRINARY_IMPL */ {"trinaryLogicImpl", NULL}, + /* PT_CLASS_ETH_IMPL */ {"expressionTypeHolderImpl", NULL}, + /* PT_CLASS_CEH_IMPL */ {"conditionalExpressionHolderImpl", NULL}, +}; + +zend_class_entry *pt_class(int idx) +{ + pt_class_ref *ref = &PT_G(class_refs)[idx]; + zend_class_entry *ce; + zend_string *name; + + if (EXPECTED(ref->ce != NULL)) { + return ref->ce; + } + + if (ref->configured != NULL) { + name = zend_string_copy(ref->configured); + } else if (ref->default_name != NULL) { + name = zend_string_init(ref->default_name, strlen(ref->default_name), 0); + } else { + zend_throw_error(NULL, "phpstan_turbo: class for '%s' was not configured", ref->key); + return NULL; + } + ce = zend_lookup_class(name); + if (ce == NULL) { + zend_throw_error(NULL, "phpstan_turbo: class %s not found", ZSTR_VAL(name)); + zend_string_release(name); + return NULL; + } + zend_string_release(name); + ref->ce = ce; + return ce; +} + +zend_class_entry *pt_impl_class(int idx, zend_class_entry *native_fallback) +{ + pt_class_ref *ref = &PT_G(class_refs)[idx]; + if (EXPECTED(ref->ce != NULL)) { + return ref->ce; + } + if (ref->configured == NULL) { + ref->ce = native_fallback; + return native_fallback; + } + return pt_class(idx); +} + +void pt_class_map_configure(zend_string *key, zend_string *value) +{ + for (int idx = 0; idx < PT_CLASS_COUNT; idx++) { + if (strcmp(ZSTR_VAL(key), pt_class_templates[idx].key) == 0) { + pt_class_ref *ref = &PT_G(class_refs)[idx]; + if (ref->configured != NULL) { + zend_string_release(ref->configured); + } + ref->configured = zend_string_copy(value); + ref->ce = NULL; + return; + } + } +} + +/* }}} */ + +/* {{{ lifecycle */ + +zend_string *pt_str_cache_printer = nullptr; +zend_string *pt_str_contains_super_global = nullptr; +zend_string *pt_str_array_map_args = nullptr; +zend_string *pt_str_start_file_pos = nullptr; +zend_string *pt_str_keep_void = nullptr; +static bool pt_strs_inited = false; + +static HashTable pt_node_class_cache; +static bool pt_node_class_cache_inited = false; + +void pt_init_strs() +{ + if (pt_strs_inited) { + return; + } + pt_str_cache_printer = zend_string_init("phpstan_cache_printer", sizeof("phpstan_cache_printer") - 1, 0); + pt_str_contains_super_global = zend_string_init("containsSuperGlobal", sizeof("containsSuperGlobal") - 1, 0); + pt_str_array_map_args = zend_string_init("arrayMapArgs", sizeof("arrayMapArgs") - 1, 0); + pt_str_start_file_pos = zend_string_init("startFilePos", sizeof("startFilePos") - 1, 0); + pt_str_keep_void = zend_string_init("keepVoid", sizeof("keepVoid") - 1, 0); + pt_strs_inited = true; +} + +void pt_support_rinit() +{ + PT_G(trinary_inited) = false; + PT_G(verbosity_inited) = false; + ZVAL_UNDEF(&PT_G(trinary_yes)); + ZVAL_UNDEF(&PT_G(trinary_maybe)); + ZVAL_UNDEF(&PT_G(trinary_no)); + ZVAL_UNDEF(&PT_G(verbosity_precise)); + + for (int i = 0; i < PT_CLASS_COUNT; i++) { + PT_G(class_refs)[i].key = pt_class_templates[i].key; + PT_G(class_refs)[i].default_name = pt_class_templates[i].default_name; + PT_G(class_refs)[i].configured = NULL; + PT_G(class_refs)[i].ce = NULL; + } + + pt_strs_inited = false; + pt_node_class_cache_inited = false; +} + +void pt_support_rshutdown() +{ + if (PT_G(trinary_inited)) { + zval_ptr_dtor(&PT_G(trinary_yes)); + zval_ptr_dtor(&PT_G(trinary_maybe)); + zval_ptr_dtor(&PT_G(trinary_no)); + PT_G(trinary_inited) = false; + } + if (PT_G(verbosity_inited)) { + zval_ptr_dtor(&PT_G(verbosity_precise)); + PT_G(verbosity_inited) = false; + } + for (int i = 0; i < PT_CLASS_COUNT; i++) { + if (PT_G(class_refs)[i].configured != NULL) { + zend_string_release(PT_G(class_refs)[i].configured); + PT_G(class_refs)[i].configured = NULL; + } + PT_G(class_refs)[i].ce = NULL; + } + if (pt_strs_inited) { + zend_string_release(pt_str_cache_printer); + zend_string_release(pt_str_contains_super_global); + zend_string_release(pt_str_array_map_args); + zend_string_release(pt_str_start_file_pos); + zend_string_release(pt_str_keep_void); + pt_strs_inited = false; + } + if (pt_node_class_cache_inited) { + zend_hash_destroy(&pt_node_class_cache); + pt_node_class_cache_inited = false; + } +} + +/* }}} */ + +/* {{{ TrinaryLogic singletons */ + +zval *pt_trinary_singleton(zend_long value) +{ + if (UNEXPECTED(!PT_G(trinary_inited))) { + static const zend_long values[3] = {PT_TRI_YES, PT_TRI_MAYBE, PT_TRI_NO}; + zend_class_entry *impl = pt_impl_class(PT_CLASS_TRINARY_IMPL, pt_ce_trinary); + zval *slots[3]; + slots[0] = &PT_G(trinary_yes); + slots[1] = &PT_G(trinary_maybe); + slots[2] = &PT_G(trinary_no); + for (int i = 0; i < 3; i++) { + object_init_ex(slots[i], impl); + ZVAL_LONG(OBJ_PROP_NUM(Z_OBJ_P(slots[i]), PT_TRI_PROP_VALUE), values[i]); + } + PT_G(trinary_inited) = true; + } + + if (value == PT_TRI_YES) { + return &PT_G(trinary_yes); + } + if (value == PT_TRI_MAYBE) { + return &PT_G(trinary_maybe); + } + return &PT_G(trinary_no); +} + +/* }}} */ + +/* {{{ userland callback helpers */ + +zend_function *pt_find_method(zend_class_entry *ce, const char *lcname, size_t len) +{ + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", ZSTR_VAL(ce->name), lcname); + } + return fn; +} + +bool pt_call_type_equals(zval *type_a, zval *type_b) +{ + zend_class_entry *ce = Z_OBJCE_P(type_a); + zend_function *fn = pt_find_method(ce, "equals", sizeof("equals") - 1); + zval ret, args[1]; + bool result; + + if (UNEXPECTED(fn == NULL)) { + return false; + } + ZVAL_COPY_VALUE(&args[0], type_b); + zend_call_known_function(fn, Z_OBJ_P(type_a), ce, &ret, 1, args, NULL); + if (UNEXPECTED(EG(exception))) { + return false; + } + result = Z_TYPE(ret) == IS_TRUE; + zval_ptr_dtor(&ret); + return result; +} + +bool pt_types_identical_or_equal(zval *type_a, zval *type_b) +{ + if (Z_OBJ_P(type_a) == Z_OBJ_P(type_b)) { + return true; + } + return pt_call_type_equals(type_a, type_b); +} + +bool pt_type_combinator_binary(const char *lcname, size_t len, zval *type_a, zval *type_b, zval *result) +{ + zend_class_entry *ce = pt_class(PT_CLASS_TYPE_COMBINATOR); + zend_function *fn; + zval args[2]; + + if (UNEXPECTED(ce == NULL)) { + return false; + } + fn = pt_find_method(ce, lcname, len); + if (UNEXPECTED(fn == NULL)) { + return false; + } + ZVAL_COPY_VALUE(&args[0], type_a); + ZVAL_COPY_VALUE(&args[1], type_b); + zend_call_known_function(fn, NULL, ce, result, 2, args, NULL); + return !EG(exception); +} + +bool pt_type_describe_precise(zval *type, zval *result) +{ + zend_class_entry *ce; + zend_function *fn; + zval args[1]; + + if (UNEXPECTED(!PT_G(verbosity_inited))) { + zend_class_entry *vce = pt_class(PT_CLASS_VERBOSITY_LEVEL); + if (UNEXPECTED(vce == NULL)) { + return false; + } + fn = pt_find_method(vce, "precise", sizeof("precise") - 1); + if (UNEXPECTED(fn == NULL)) { + return false; + } + zend_call_known_function(fn, NULL, vce, &PT_G(verbosity_precise), 0, NULL, NULL); + if (UNEXPECTED(EG(exception))) { + return false; + } + PT_G(verbosity_inited) = true; + } + + ce = Z_OBJCE_P(type); + fn = pt_find_method(ce, "describe", sizeof("describe") - 1); + if (UNEXPECTED(fn == NULL)) { + return false; + } + ZVAL_COPY_VALUE(&args[0], &PT_G(verbosity_precise)); + zend_call_known_function(fn, Z_OBJ_P(type), ce, result, 1, args, NULL); + return !EG(exception); +} + +void pt_throw_should_not_happen() +{ + zend_class_entry *ce = pt_class(PT_CLASS_SHOULD_NOT_HAPPEN); + if (ce == NULL) { + return; /* error already thrown */ + } + zend_throw_exception(ce, "Internal error.", 0); +} + +bool pt_call_scope_bool(zval *scope, const char *lcname, size_t len, uint32_t argc, zval *argv, bool *out) +{ + zend_class_entry *ce = Z_OBJCE_P(scope); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, lcname, len); + zval ret; + + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: method %s::%s not found", ZSTR_VAL(ce->name), lcname); + return false; + } + zend_call_known_function(fn, Z_OBJ_P(scope), ce, &ret, argc, argv, NULL); + if (UNEXPECTED(EG(exception))) { + return false; + } + *out = zend_is_true(&ret); + zval_ptr_dtor(&ret); + return true; +} + +/* }}} */ + +/* {{{ node class info + attributes */ + +static void pt_node_class_info_free(zval *zv) +{ + pt_node_class_info *info = (pt_node_class_info *) Z_PTR_P(zv); + if (info->subnode_offsets != NULL) { + efree(info->subnode_offsets); + } + efree(info); +} + +int32_t pt_instance_prop_offset(zend_class_entry *ce, const char *name, size_t len) +{ + zend_property_info *info = (zend_property_info *) zend_hash_str_find_ptr(&ce->properties_info, name, len); + if (info == NULL || (info->flags & ZEND_ACC_STATIC) != 0) { + return -1; + } + return (int32_t) info->offset; +} + +pt_node_class_info *pt_get_node_class_info(zend_class_entry *ce) +{ + pt_node_class_info *info; + zend_class_entry *variable_ce; + + if (!pt_node_class_cache_inited) { + zend_hash_init(&pt_node_class_cache, 64, NULL, pt_node_class_info_free, 0); + pt_node_class_cache_inited = true; + } + + info = (pt_node_class_info *) zend_hash_find_ptr(&pt_node_class_cache, ce->name); + if (EXPECTED(info != NULL)) { + return info; + } + + info = (pt_node_class_info *) ecalloc(1, sizeof(pt_node_class_info)); + info->attributes_offset = pt_instance_prop_offset(ce, "attributes", sizeof("attributes") - 1); + info->name_offset = pt_instance_prop_offset(ce, "name", sizeof("name") - 1); + + variable_ce = pt_class(PT_CLASS_VARIABLE); + if (variable_ce == NULL) { + efree(info); + return NULL; + } + info->is_variable = instanceof_function(ce, variable_ce); + + zend_hash_add_ptr(&pt_node_class_cache, ce->name, info); + return info; +} + +pt_node_class_info *pt_node_class_info_for_object(zend_object *obj) +{ + zend_class_entry *ce = obj->ce; + pt_node_class_info *info = pt_get_node_class_info(ce); + zend_function *fn; + zval retval; + + if (info == NULL) { + return NULL; + } + if (info->subnode_offsets != NULL || info->subnode_count == UINT32_MAX) { + return info; + } + + fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "getsubnodenames", sizeof("getsubnodenames") - 1); + if (fn == NULL || (fn->common.fn_flags & ZEND_ACC_ABSTRACT) != 0) { + info->subnode_count = UINT32_MAX; + return info; + } + + zend_call_known_function(fn, obj, ce, &retval, 0, NULL, NULL); + if (EG(exception) || Z_TYPE(retval) != IS_ARRAY) { + zval_ptr_dtor(&retval); + info->subnode_count = UINT32_MAX; + return info; + } + + { + HashTable *names = Z_ARRVAL(retval); + uint32_t count = zend_hash_num_elements(names); + uint32_t i = 0; + zval *name_zv; + + info->subnode_offsets = (uint32_t *) emalloc(sizeof(uint32_t) * (count > 0 ? count : 1)); + ZEND_HASH_FOREACH_VAL(names, name_zv) { + int32_t off; + if (Z_TYPE_P(name_zv) != IS_STRING) { + continue; + } + off = pt_instance_prop_offset(ce, Z_STRVAL_P(name_zv), Z_STRLEN_P(name_zv)); + if (off >= 0) { + info->subnode_offsets[i++] = (uint32_t) off; + } + } ZEND_HASH_FOREACH_END(); + info->subnode_count = i; + } + zval_ptr_dtor(&retval); + return info; +} + +zval *pt_node_attribute(zend_object *node, zend_string *name) +{ + pt_node_class_info *info = pt_get_node_class_info(node->ce); + zval *attrs; + + if (info == NULL || info->attributes_offset < 0) { + return NULL; + } + attrs = OBJ_PROP(node, info->attributes_offset); + ZVAL_DEREF(attrs); + if (Z_TYPE_P(attrs) != IS_ARRAY) { + return NULL; + } + return zend_hash_find(Z_ARRVAL_P(attrs), name); +} + +bool pt_node_set_attribute(zend_object *node, zend_string *name, zval *value) +{ + pt_node_class_info *info = pt_get_node_class_info(node->ce); + zval *attrs; + + if (info == NULL || info->attributes_offset < 0) { + return false; + } + attrs = OBJ_PROP(node, info->attributes_offset); + ZVAL_DEREF(attrs); + if (Z_TYPE_P(attrs) != IS_ARRAY) { + return false; + } + SEPARATE_ARRAY(attrs); + Z_TRY_ADDREF_P(value); + zend_hash_update(Z_ARRVAL_P(attrs), name, value); + return true; +} + +/* }}} */ + +/* {{{ node key */ + +static bool pt_call_print_expr(zval *expr_printer, zend_object *node, zval *result) +{ + zend_class_entry *ce = Z_OBJCE_P(expr_printer); + zend_function *fn = (zend_function *) zend_hash_str_find_ptr(&ce->function_table, "printexpr", sizeof("printexpr") - 1); + zval arg; + + if (UNEXPECTED(fn == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: printExpr not found"); + return false; + } + ZVAL_OBJ(&arg, node); + zend_call_known_function(fn, Z_OBJ_P(expr_printer), ce, result, 1, &arg, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(result); + return false; + } + if (UNEXPECTED(Z_TYPE_P(result) != IS_STRING)) { + zval_ptr_dtor(result); + zend_throw_error(NULL, "phpstan_turbo: printExpr did not return a string"); + return false; + } + return true; +} + +/* The printed form of the expression, without pt_node_key's attribute-derived + * suffixes — the equivalent of a plain $exprPrinter->printExpr($node) call + * (which caches through the printer attribute). Owned string; NULL on + * failure with an exception pending. */ +static zend_string *pt_node_printed_expr(zend_object *node, zval *expr_printer) +{ + pt_node_class_info *info = pt_get_node_class_info(node->ce); + + if (info == NULL) { + return NULL; + } + + /* fast path: '$' . $node->name for Variable with a string name */ + if (info->is_variable && info->name_offset >= 0) { + zval *name = OBJ_PROP(node, info->name_offset); + ZVAL_DEREF(name); + if (Z_TYPE_P(name) == IS_STRING) { + zend_string *name_str = Z_STR_P(name); + zend_string *key = zend_string_alloc(ZSTR_LEN(name_str) + 1, 0); + ZSTR_VAL(key)[0] = '$'; + memcpy(ZSTR_VAL(key) + 1, ZSTR_VAL(name_str), ZSTR_LEN(name_str)); + ZSTR_VAL(key)[ZSTR_LEN(key)] = '\0'; + return key; + } + } + + zval *attr = pt_node_attribute(node, pt_str_cache_printer); + if (attr != NULL && Z_TYPE_P(attr) == IS_STRING) { + return zend_string_copy(Z_STR_P(attr)); + } + + zval printed; + if (!pt_call_print_expr(expr_printer, node, &printed)) { + return NULL; + } + return Z_STR(printed); /* take ownership */ +} + +zend_string *pt_node_key(zend_object *node, zval *expr_printer) +{ + zend_string *key; + + pt_init_strs(); + + /* the Variable fast path returns before any suffix handling below, same + * as the twin: a Variable node never carries the suffix attributes */ + pt_node_class_info *info = pt_get_node_class_info(node->ce); + if (info == NULL) { + return NULL; + } + if (info->is_variable && info->name_offset >= 0) { + zval *name = OBJ_PROP(node, info->name_offset); + ZVAL_DEREF(name); + if (Z_TYPE_P(name) == IS_STRING) { + return pt_node_printed_expr(node, expr_printer); + } + } + + key = pt_node_printed_expr(node, expr_printer); + if (key == NULL) { + return NULL; + } + + /* FunctionLike with arrayMapArgs + startFilePos: append the array_map + * argument suffix exactly like MutatingScope::getNodeKey() */ + { + zend_class_entry *fl_ce = pt_class(PT_CLASS_FUNCTION_LIKE); + if (fl_ce == NULL) { + zend_string_release(key); + return NULL; + } + if (instanceof_function(node->ce, fl_ce)) { + zval *map_args = pt_node_attribute(node, pt_str_array_map_args); + zval *start_pos = pt_node_attribute(node, pt_str_start_file_pos); + if (map_args != NULL && Z_TYPE_P(map_args) != IS_NULL + && start_pos != NULL && Z_TYPE_P(start_pos) != IS_NULL) { + smart_str str = {}; + smart_str_append(&str, key); + smart_str_appendl(&str, "/*", 2); + if (Z_TYPE_P(start_pos) == IS_LONG) { + smart_str_append_long(&str, Z_LVAL_P(start_pos)); + } + if (Z_TYPE_P(map_args) == IS_ARRAY) { + zval *arg; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(map_args), arg) { + zval *arg_deref = arg; + zval *value_prop; + ZVAL_DEREF(arg_deref); + if (Z_TYPE_P(arg_deref) != IS_OBJECT) { + continue; + } + { + int32_t voff = pt_instance_prop_offset(Z_OBJCE_P(arg_deref), "value", sizeof("value") - 1); + if (voff < 0) { + continue; + } + value_prop = OBJ_PROP(Z_OBJ_P(arg_deref), voff); + ZVAL_DEREF(value_prop); + } + if (Z_TYPE_P(value_prop) != IS_OBJECT) { + continue; + } + smart_str_appendc(&str, ':'); + { + /* plain printExpr like the twin — NOT the full node + * key: an argument carrying its own suffix + * attributes must not have them appended here */ + zend_string *arg_key = pt_node_printed_expr(Z_OBJ_P(value_prop), expr_printer); + if (arg_key == NULL) { + smart_str_free(&str); + zend_string_release(key); + return NULL; + } + smart_str_append(&str, arg_key); + zend_string_release(arg_key); + } + } ZEND_HASH_FOREACH_END(); + } + smart_str_appendl(&str, "*/", 2); + zend_string_release(key); + key = smart_str_extract(&str); + } + } + } + + { + zval *keep_void = pt_node_attribute(node, pt_str_keep_void); + if (keep_void != NULL && Z_TYPE_P(keep_void) == IS_TRUE) { + smart_str str = {}; + smart_str_append(&str, key); + smart_str_appendl(&str, "/*keepVoid*/", sizeof("/*keepVoid*/") - 1); + zend_string_release(key); + key = smart_str_extract(&str); + } + } + + return key; +} + +/* }}} */ + +/* {{{ findFirst walker + superglobal scan */ + +zend_object *pt_find_first_recursive(zend_object *node, pt_node_matcher matcher, void *ctx) +{ + pt_node_class_info *info; + zend_class_entry *node_iface; + uint32_t i; + + if (matcher(node, ctx)) { + return node; + } + if (UNEXPECTED(((pt_find_ctx *) ctx)->failed)) { + return NULL; + } + + info = pt_node_class_info_for_object(node); + if (info == NULL || !PT_HAS_SUBNODES(info)) { + return NULL; + } + + node_iface = pt_class(PT_CLASS_NODE); + if (UNEXPECTED(node_iface == NULL)) { + ((pt_find_ctx *) ctx)->failed = true; + return NULL; + } + + for (i = 0; i < info->subnode_count; i++) { + zval *val = OBJ_PROP(node, info->subnode_offsets[i]); + ZVAL_DEREF(val); + if (Z_TYPE_P(val) == IS_OBJECT) { + if (instanceof_function(Z_OBJCE_P(val), node_iface)) { + zend_object *found = pt_find_first_recursive(Z_OBJ_P(val), matcher, ctx); + if (found != NULL || ((pt_find_ctx *) ctx)->failed) { + return found; + } + } + } else if (Z_TYPE_P(val) == IS_ARRAY) { + zval *el; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(val), el) { + zval *el_deref = el; + ZVAL_DEREF(el_deref); + if (Z_TYPE_P(el_deref) == IS_OBJECT && instanceof_function(Z_OBJCE_P(el_deref), node_iface)) { + zend_object *found = pt_find_first_recursive(Z_OBJ_P(el_deref), matcher, ctx); + if (found != NULL || ((pt_find_ctx *) ctx)->failed) { + return found; + } + } + } ZEND_HASH_FOREACH_END(); + } + } + return NULL; +} + +static const struct { const char *name; size_t len; } pt_superglobals[] = { + {"GLOBALS", 7}, + {"_SERVER", 7}, + {"_GET", 4}, + {"_POST", 5}, + {"_FILES", 6}, + {"_COOKIE", 7}, + {"_SESSION", 8}, + {"_REQUEST", 8}, + {"_ENV", 4}, +}; + +bool pt_is_superglobal_name(zend_string *name) +{ + for (size_t i = 0; i < sizeof(pt_superglobals) / sizeof(pt_superglobals[0]); i++) { + if (ZSTR_LEN(name) == pt_superglobals[i].len + && memcmp(ZSTR_VAL(name), pt_superglobals[i].name, pt_superglobals[i].len) == 0) { + return true; + } + } + return false; +} + +static bool pt_superglobal_matcher(zend_object *node, void *ctx) +{ + pt_node_class_info *info = pt_get_node_class_info(node->ce); + zval *name; + + (void) ctx; + if (info == NULL || !info->is_variable || info->name_offset < 0) { + return false; + } + name = OBJ_PROP(node, info->name_offset); + ZVAL_DEREF(name); + if (Z_TYPE_P(name) != IS_STRING) { + return false; + } + return pt_is_superglobal_name(Z_STR_P(name)); +} + +bool pt_expr_contains_superglobal(zend_object *expr) +{ + zval *attr; + pt_find_ctx ctx; + bool contains; + zval attr_val; + + pt_init_strs(); + + attr = pt_node_attribute(expr, pt_str_contains_super_global); + if (attr != NULL && (Z_TYPE_P(attr) == IS_TRUE || Z_TYPE_P(attr) == IS_FALSE)) { + return Z_TYPE_P(attr) == IS_TRUE; + } + + memset(&ctx, 0, sizeof(ctx)); + contains = pt_find_first_recursive(expr, pt_superglobal_matcher, &ctx) != NULL; + ZVAL_BOOL(&attr_val, contains); + pt_node_set_attribute(expr, pt_str_contains_super_global, &attr_val); + return contains; +} + +/* }}} */ + +/* {{{ holder helpers */ + +bool pt_check_holder(zval *zv) +{ + if (UNEXPECTED(Z_TYPE_P(zv) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(zv), pt_ce_expr_type_holder))) { + zend_type_error("phpstan_turbo: expected ExpressionTypeHolder, got %s", zend_zval_value_name(zv)); + return false; + } + return true; +} + +void pt_holder_create(zval *result, zval *expr, zval *type, zend_long certainty) +{ + zend_class_entry *impl = pt_impl_class(PT_CLASS_ETH_IMPL, pt_ce_expr_type_holder); + zend_object *obj; + object_init_ex(result, impl); + obj = Z_OBJ_P(result); + ZVAL_COPY(OBJ_PROP_NUM(obj, PT_ETH_PROP_EXPR), expr); + ZVAL_COPY(OBJ_PROP_NUM(obj, PT_ETH_PROP_TYPE), type); + ZVAL_COPY(OBJ_PROP_NUM(obj, PT_ETH_PROP_CERTAINTY), pt_trinary_singleton(certainty)); +} + +bool pt_holder_and(zval *a, zval *b, zval *result) +{ + zend_object *ao = Z_OBJ_P(a); + zend_object *bo = Z_OBJ_P(b); + zval *a_type = OBJ_PROP_NUM(ao, PT_ETH_PROP_TYPE); + zval *b_type = OBJ_PROP_NUM(bo, PT_ETH_PROP_TYPE); + zend_long ac = pt_holder_certainty_value(ao); + zend_long bc = pt_holder_certainty_value(bo); + + if (pt_types_identical_or_equal(a_type, b_type)) { + if ((ac & bc) == PT_TRI_YES || ac == PT_TRI_MAYBE) { + ZVAL_COPY(result, a); + } else { + ZVAL_COPY(result, b); + } + return true; + } + if (UNEXPECTED(EG(exception))) { + return false; + } + { + zval union_type; + if (UNEXPECTED(!pt_type_combinator_binary("union", sizeof("union") - 1, a_type, b_type, &union_type))) { + return false; + } + pt_holder_create(result, OBJ_PROP_NUM(ao, PT_ETH_PROP_EXPR), &union_type, ac & bc); + zval_ptr_dtor(&union_type); + } + return true; +} + +bool pt_holder_equals(zval *a, zval *b, bool *out) +{ + zend_object *ao = Z_OBJ_P(a); + zend_object *bo = Z_OBJ_P(b); + + if (ao == bo) { + *out = true; + return true; + } + if (pt_holder_certainty_value(ao) != pt_holder_certainty_value(bo)) { + *out = false; + return true; + } + *out = pt_types_identical_or_equal(OBJ_PROP_NUM(ao, PT_ETH_PROP_TYPE), OBJ_PROP_NUM(bo, PT_ETH_PROP_TYPE)); + return !EG(exception); +} + +bool pt_holder_equal_types(zval *a, zval *b, bool *out) +{ + zend_object *ao = Z_OBJ_P(a); + zend_object *bo = Z_OBJ_P(b); + if (ao == bo) { + *out = true; + return true; + } + *out = pt_types_identical_or_equal(OBJ_PROP_NUM(ao, PT_ETH_PROP_TYPE), OBJ_PROP_NUM(bo, PT_ETH_PROP_TYPE)); + return !EG(exception); +} + +zend_string *pt_ceh_key_build(HashTable *conds, zval *type_holder) +{ + smart_str str = {}; + zend_string *key_str; + zend_ulong kidx; + zval *entry; + bool first = true; + + ZEND_HASH_FOREACH_KEY_VAL(conds, kidx, key_str, entry) { + zval described; + zval *entry_deref = entry; + ZVAL_DEREF(entry_deref); + if (!first) { + smart_str_appendl(&str, " && ", 4); + } + first = false; + if (key_str != NULL) { + smart_str_append(&str, key_str); + } else { + smart_str_append_long(&str, (zend_long) kidx); + } + smart_str_appendc(&str, '='); + if (UNEXPECTED(!pt_type_describe_precise(OBJ_PROP_NUM(Z_OBJ_P(entry_deref), PT_ETH_PROP_TYPE), &described))) { + smart_str_free(&str); + return NULL; + } + if (EXPECTED(Z_TYPE(described) == IS_STRING)) { + smart_str_append(&str, Z_STR(described)); + } + zval_ptr_dtor(&described); + } ZEND_HASH_FOREACH_END(); + + smart_str_appendl(&str, " => ", 4); + { + zval described; + if (UNEXPECTED(!pt_type_describe_precise(OBJ_PROP_NUM(Z_OBJ_P(type_holder), PT_ETH_PROP_TYPE), &described))) { + smart_str_free(&str); + return NULL; + } + if (EXPECTED(Z_TYPE(described) == IS_STRING)) { + smart_str_append(&str, Z_STR(described)); + } + zval_ptr_dtor(&described); + } + smart_str_appendl(&str, " (", 2); + { + zend_long certainty = pt_holder_certainty_value(Z_OBJ_P(type_holder)); + if (certainty == PT_TRI_YES) { + smart_str_appendl(&str, "Yes", 3); + } else if (certainty == PT_TRI_MAYBE) { + smart_str_appendl(&str, "Maybe", 5); + } else { + smart_str_appendl(&str, "No", 2); + } + } + smart_str_appendc(&str, ')'); + + return smart_str_extract(&str); +} + +/* }}} */ diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h new file mode 100644 index 00000000000..fef7a3ea9cf --- /dev/null +++ b/turbo-ext/src/support.h @@ -0,0 +1,315 @@ +/* + * phpstan_turbo — shared native support layer. + * + * Infrastructure used by all native classes: the configurable class map, + * TrinaryLogic singletons, callback helpers into userland (TypeCombinator, + * Type::equals, describe), the per-class node info cache (subnode property + * offsets, attribute table offset), native node attributes, expression keys, + * the recursive findFirst walker and ExpressionTypeHolder helpers. + * + * The function names deliberately match the proven C implementation this + * extension was ported from (turbo-ext/native at the time of the port) so the + * two stay diffable. + */ + +#ifndef PHPSTANTURBO_SUPPORT_H +#define PHPSTANTURBO_SUPPORT_H + +/* Third-party headers (PHP-CPP, the Zend engine) are not warning-clean under + * the strict flags this extension is built with in CI; exempt them without + * relaxing the flags for our own code. The -Wpragmas / -Wunknown-warning-option + * ignores make the compiler-specific entries below portable across gcc/clang. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunknown-warning-option" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Winconsistent-missing-override" +#pragma GCC diagnostic ignored "-Wignored-qualifiers" +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +#include + +extern "C" { +#include "php.h" +#include "zend_exceptions.h" +#include "zend_interfaces.h" +#include "zend_smart_str.h" +} + +#pragma GCC diagnostic pop + +/* {{{ configurable class references */ + +typedef struct _pt_class_ref { + const char *key; /* key in the Runtime::configure() map */ + const char *default_name; /* fallback FQCN when not configured */ + zend_string *configured; /* name set via configure(), owned */ + zend_class_entry *ce; /* resolved entry, per-request cache */ +} pt_class_ref; + +enum { + PT_CLASS_TYPE_COMBINATOR = 0, + PT_CLASS_BOOLEAN_TYPE, + PT_CLASS_CONSTANT_BOOLEAN_TYPE, + PT_CLASS_SHOULD_NOT_HAPPEN, + PT_CLASS_VERBOSITY_LEVEL, + PT_CLASS_VARIABLE, + PT_CLASS_FUNC_CALL, + PT_CLASS_VIRTUAL_NODE, + PT_CLASS_NODE, + PT_CLASS_NAME, + PT_CLASS_EXPR, + PT_CLASS_PROPERTY_FETCH, + PT_CLASS_INTERTWINED_VAR, + PT_CLASS_ARRAY_DIM_FETCH, + PT_CLASS_METHOD_CALL, + PT_CLASS_FUNCTION_LIKE, + PT_CLASS_CALL_LIKE, + PT_CLASS_STATIC_CALL, + PT_CLASS_NEW, + PT_CLASS_CLASS_STMT, + PT_CLASS_VARIADIC_PLACEHOLDER, + PT_CLASS_ERROR_TYPE, + PT_CLASS_SCALAR, + PT_CLASS_ARRAY_EXPR, + PT_CLASS_UNARY_MINUS, + PT_CLASS_YIELD, + PT_CLASS_YIELD_FROM, + PT_CLASS_STMT, + PT_CLASS_NODE_VISITOR_ABSTRACT, + PT_CLASS_CLOSURE_EXPR, + PT_CLASS_ARROW_FUNCTION, + PT_CLASS_TYPE, + PT_CLASS_RECURSION_GUARD, + /* classes the extension instantiates; configured to the stub subclasses + * so created objects satisfy the original PHPStan type hints */ + PT_CLASS_TRINARY_IMPL, + PT_CLASS_ETH_IMPL, + PT_CLASS_CEH_IMPL, + PT_CLASS_COUNT +}; + +/* Resolves a configured/default class; throws and returns NULL on failure. */ +zend_class_entry *pt_class(int idx); + +/* Like pt_class(), but for the *Impl entries: when unconfigured, falls back + * to the given native class entry instead of a name lookup. */ +zend_class_entry *pt_impl_class(int idx, zend_class_entry *native_fallback); + +/* Called by Runtime::configure() */ +void pt_class_map_configure(zend_string *key, zend_string *value); + +/* }}} */ + +/* {{{ globals (NTS; the extension targets the CLI like the analysis itself) */ + +struct pt_globals_t { + zval trinary_yes; + zval trinary_maybe; + zval trinary_no; + bool trinary_inited; + zval verbosity_precise; + bool verbosity_inited; + pt_class_ref class_refs[PT_CLASS_COUNT]; +}; + +extern pt_globals_t pt_globals; + +#define PT_G(v) (pt_globals.v) + +/* per-request lifecycle, wired to PHP-CPP's onRequest/onIdle */ +void pt_support_rinit(); +void pt_support_rshutdown(); + +/* }}} */ + +/* {{{ native class entries (registered in the per-class files) */ + +extern zend_class_entry *pt_ce_trinary; +extern zend_class_entry *pt_ce_expr_type_holder; +extern zend_class_entry *pt_ce_cond_expr_holder; +extern zend_class_entry *pt_ce_type_combinator_cache; + +/* registration hooks, called from the extension's onStartup */ +void pt_register_trinary_logic(); +void pt_register_expression_type_holder(); +void pt_register_conditional_expression_holder(); +void pt_register_combinations_helper(); +void pt_register_node_traverser(); +void pt_register_scope_ops(); +void pt_register_node_scanner(); +void pt_register_parser_runner(); +void pt_register_type_combinator_cache(); + +/* per-request hooks of individual classes */ +void pt_node_traverser_rinit(); +void pt_node_traverser_rshutdown(); +void pt_scope_ops_rinit(); +void pt_scope_ops_rshutdown(); +void pt_type_combinator_cache_rinit(); +void pt_type_combinator_cache_rshutdown(); + +/* }}} */ + +/* {{{ TrinaryLogic values and singletons */ + +#define PT_TRI_YES 3 +#define PT_TRI_MAYBE 1 +#define PT_TRI_NO 0 + +#define PT_TRI_PROP_VALUE 0 +#define PT_ETH_PROP_EXPR 0 +#define PT_ETH_PROP_TYPE 1 +#define PT_ETH_PROP_CERTAINTY 2 +#define PT_CEH_PROP_CONDS 0 +#define PT_CEH_PROP_TYPEHOLDER 1 + +/* Returns the singleton for the given value (instances of the configured + * trinaryLogicImpl class). Borrowed zval; callers must copy. */ +zval *pt_trinary_singleton(zend_long value); + +static zend_always_inline zend_long pt_trinary_value(zend_object *obj) +{ + return Z_LVAL_P(OBJ_PROP_NUM(obj, PT_TRI_PROP_VALUE)); +} + +static zend_always_inline zend_long pt_holder_certainty_value(zend_object *holder) +{ + return pt_trinary_value(Z_OBJ_P(OBJ_PROP_NUM(holder, PT_ETH_PROP_CERTAINTY))); +} + +/* }}} */ + +/* {{{ userland callback helpers */ + +zend_function *pt_find_method(zend_class_entry *ce, const char *lcname, size_t len); +bool pt_call_type_equals(zval *type_a, zval *type_b); +bool pt_types_identical_or_equal(zval *type_a, zval *type_b); +/* TypeCombinator::($a, $b) */ +bool pt_type_combinator_binary(const char *lcname, size_t len, zval *type_a, zval *type_b, zval *result); +/* $type->describe(VerbosityLevel::precise()) */ +bool pt_type_describe_precise(zval *type, zval *result); +void pt_throw_should_not_happen(); + +/* }}} */ + +/* {{{ dual string/int key hashtable helpers + * + * Expression tables are keyed by printed expression strings, but PHP converts + * numeric-string keys to integer keys on write, so table operations must + * handle both key kinds like PHP array ops do. + */ + +static zend_always_inline zval *pt_ht_find(HashTable *ht, zend_string *skey, zend_ulong idx) +{ + return skey != NULL ? zend_hash_find(ht, skey) : zend_hash_index_find(ht, idx); +} + +static zend_always_inline bool pt_ht_exists(HashTable *ht, zend_string *skey, zend_ulong idx) +{ + return skey != NULL ? zend_hash_exists(ht, skey) : zend_hash_index_exists(ht, idx); +} + +static zend_always_inline void pt_ht_add_new(HashTable *ht, zend_string *skey, zend_ulong idx, zval *val) +{ + if (skey != NULL) { + zend_hash_add_new(ht, skey, val); + } else { + zend_hash_index_add_new(ht, idx, val); + } +} + +static zend_always_inline void pt_ht_update(HashTable *ht, zend_string *skey, zend_ulong idx, zval *val) +{ + if (skey != NULL) { + zend_hash_update(ht, skey, val); + } else { + zend_hash_index_update(ht, idx, val); + } +} + +static zend_always_inline void pt_ht_del(HashTable *ht, zend_string *skey, zend_ulong idx) +{ + if (skey != NULL) { + zend_hash_del(ht, skey); + } else { + zend_hash_index_del(ht, idx); + } +} + +/* }}} */ + +/* {{{ node class info, attributes, keys, findFirst */ + +typedef struct _pt_node_class_info { + uint32_t *subnode_offsets; + uint32_t subnode_count; /* UINT32_MAX: not resolvable */ + int32_t attributes_offset; + int32_t name_offset; + bool is_variable; +} pt_node_class_info; + +#define PT_HAS_SUBNODES(info) ((info)->subnode_offsets != NULL && (info)->subnode_count != UINT32_MAX) + +/* cheap info (offsets/flags); does not resolve subnodes */ +pt_node_class_info *pt_get_node_class_info(zend_class_entry *ce); +/* resolves subnode offsets on first sight of an instance */ +pt_node_class_info *pt_node_class_info_for_object(zend_object *obj); + +int32_t pt_instance_prop_offset(zend_class_entry *ce, const char *name, size_t len); + +/* cached attribute-name strings, created lazily per request */ +extern zend_string *pt_str_cache_printer; +extern zend_string *pt_str_contains_super_global; +extern zend_string *pt_str_array_map_args; +extern zend_string *pt_str_start_file_pos; +extern zend_string *pt_str_keep_void; +void pt_init_strs(); + +zval *pt_node_attribute(zend_object *node, zend_string *name); +bool pt_node_set_attribute(zend_object *node, zend_string *name, zval *value); + +/* Expression key for the node (MutatingScope::getNodeKey semantics); the PHP + * ExprPrinter is called on cache misses. Returned string owned by caller; + * NULL on failure (exception thrown). */ +zend_string *pt_node_key(zend_object *node, zval *expr_printer); + +typedef struct _pt_find_ctx { + zend_class_entry *target_ce; + zend_string *invalidate_str; + zval *expr_printer; + bool is_this; + zval *scope; + zval class_reflection; + bool class_reflection_fetched; + bool failed; +} pt_find_ctx; + +typedef bool (*pt_node_matcher)(zend_object *node, void *ctx); + +zend_object *pt_find_first_recursive(zend_object *node, pt_node_matcher matcher, void *ctx); + +bool pt_is_superglobal_name(zend_string *name); +/* CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME-cached superglobal scan */ +bool pt_expr_contains_superglobal(zend_object *expr); + +/* }}} */ + +/* {{{ ExpressionTypeHolder helpers (no zpp; used by holders and ScopeOps) */ + +bool pt_check_holder(zval *zv); +/* creates an instance of the configured expressionTypeHolderImpl class */ +void pt_holder_create(zval *result, zval *expr, zval *type, zend_long certainty); +bool pt_holder_and(zval *a, zval *b, zval *result); +bool pt_holder_equals(zval *a, zval *b, bool *out); +bool pt_holder_equal_types(zval *a, zval *b, bool *out); + +/* ConditionalExpressionHolder::getKey() builder, shared with ScopeOps */ +zend_string *pt_ceh_key_build(HashTable *conds, zval *type_holder); + +/* calls a (possibly private) method on the scope, coercing result to bool */ +bool pt_call_scope_bool(zval *scope, const char *lcname, size_t len, uint32_t argc, zval *argv, bool *out); + +/* }}} */ + +#endif /* PHPSTANTURBO_SUPPORT_H */ diff --git a/turbo-ext/src/zv.h b/turbo-ext/src/zv.h new file mode 100644 index 00000000000..070681ff347 --- /dev/null +++ b/turbo-ext/src/zv.h @@ -0,0 +1,545 @@ +/* + * Zero-cost C++ wrappers over zvals, hashtables and zend objects, so the + * native implementations can read like the PHP code they replace while + * compiling to exactly the raw-macro instructions. + * + * Rules that keep this layer free: + * - everything is header-inline and trivially small; no virtuals anywhere + * - Ref/ArrRef/ObjRef/StrRef are borrowed views: a single pointer, trivially + * copyable, no ownership, no destructor work + * - Val (and Arr) are OWNED values: move-only RAII; the destructor releases. + * Ownership transfer is spelled std::move / take(), never a hidden copy + * - never allocate in this layer beyond what the wrapped zend call allocates + * + * The borrowed/owned discipline of the pn_ and pt_ helper families becomes + * types: a function taking Ref borrows, a function taking Val consumes, a + * function returning Val transfers ownership to the caller. + */ + +#ifndef PHPSTANTURBO_ZV_H +#define PHPSTANTURBO_ZV_H + +#include "support.h" + +#include + +namespace zv { + +class Val; +class Arr; + +/* Borrowed view of any zval. */ +class Ref +{ +protected: + zval *z; + +public: + explicit Ref(zval *zvp) : z(zvp) {} + + zval *raw() const { return z; } + + bool isUndef() const { return Z_TYPE_P(z) == IS_UNDEF; } + /* PHP null; UNDEF counts as null the same way the pn_ helpers treated it */ + bool isNull() const { return Z_TYPE_P(z) == IS_NULL || Z_TYPE_P(z) == IS_UNDEF; } + bool isBool() const { return Z_TYPE_P(z) == IS_TRUE || Z_TYPE_P(z) == IS_FALSE; } + bool isTrue() const { return Z_TYPE_P(z) == IS_TRUE; } + bool isFalse() const { return Z_TYPE_P(z) == IS_FALSE; } + bool isLong() const { return Z_TYPE_P(z) == IS_LONG; } + bool isString() const { return Z_TYPE_P(z) == IS_STRING; } + bool isArray() const { return Z_TYPE_P(z) == IS_ARRAY; } + bool isObject() const { return Z_TYPE_P(z) == IS_OBJECT; } + + zend_long asLong() const { return Z_LVAL_P(z); } + zend_long toLong() const { return zval_get_long(z); } + zend_string *asString() const { return Z_STR_P(z); } + zend_object *asObject() const { return Z_OBJ_P(z); } + HashTable *asArrayTable() const { return Z_ARRVAL_P(z); } + + bool stringEquals(const char *literal, size_t len) const + { + return isString() && zend_string_equals_cstr(Z_STR_P(z), literal, len); + } + + /* length-deducing overload for string literals */ + template + bool stringEquals(const char (&literal)[N]) const + { + return stringEquals(literal, N - 1); + } + + bool instanceOf(zend_class_entry *ce) const + { + return isObject() && instanceof_function(Z_OBJCE_P(z), ce); + } + + /* strips references, mirroring ZVAL_DEREF at PHP boundaries */ + Ref deref() const + { + zval *d = z; + ZVAL_DEREF(d); + return Ref(d); + } + + /* $slot = $value: overwrites the viewed slot with an owned value, + * releasing the previous one. The slot must be writable — an entry of a + * separated array, a property slot of an owned object. */ + inline void assign(Val owned); +}; + +/* Owned zval: move-only RAII. Adopts an already-owned raw zval. */ +class Val +{ +protected: + zval z; + + struct Adopt + { + }; + Val(zval owned, Adopt) : z(owned) {} + +public: + Val() { ZVAL_UNDEF(&z); } + Val(const Val &) = delete; + Val &operator=(const Val &) = delete; + Val(Val &&other) noexcept : z(other.z) { ZVAL_UNDEF(&other.z); } + + Val &operator=(Val &&other) noexcept + { + if (this != &other) { + release(); + z = other.z; + ZVAL_UNDEF(&other.z); + } + return *this; + } + + ~Val() { release(); } + + void release() + { + if (!Z_ISUNDEF(z)) { + zval_ptr_dtor(&z); + ZVAL_UNDEF(&z); + } + } + + /* takes over a raw zval the caller owns */ + static Val adopt(zval owned) { return Val(owned, Adopt{}); } + + /* addref-copy of a borrowed value */ + static Val copyOf(Ref r) + { + zval c; + ZVAL_COPY(&c, r.raw()); + return Val(c, Adopt{}); + } + + static Val null() + { + zval c; + ZVAL_NULL(&c); + return Val(c, Adopt{}); + } + + static Val boolean(bool b) + { + zval c; + ZVAL_BOOL(&c, b); + return Val(c, Adopt{}); + } + + static Val integer(zend_long l) + { + zval c; + ZVAL_LONG(&c, l); + return Val(c, Adopt{}); + } + + static Val string(zend_string *borrowed) + { + zval c; + ZVAL_STR_COPY(&c, borrowed); + return Val(c, Adopt{}); + } + + static Val string(const char *s, size_t len) + { + zval c; + ZVAL_STRINGL(&c, s, len); + return Val(c, Adopt{}); + } + + /* takes over an owned zend_string (no addref), like RETURN_STR */ + static Val adoptString(zend_string *owned) + { + zval c; + ZVAL_STR(&c, owned); + return Val(c, Adopt{}); + } + + Ref ref() { return Ref(&z); } + zval *raw() { return &z; } + + /* transfers ownership out as a raw zval (for RETVAL/stack slots) */ + zval take() + { + zval out = z; + ZVAL_UNDEF(&z); + return out; + } + + /* moves this value into the engine's return_value slot */ + void intoReturnValue(zval *return_value) { ZVAL_COPY_VALUE(return_value, &z); ZVAL_UNDEF(&z); } + + bool isUndef() const { return Z_TYPE(z) == IS_UNDEF; } + bool isNull() const { return Z_TYPE(z) == IS_NULL || Z_TYPE(z) == IS_UNDEF; } +}; + +inline void Ref::assign(Val owned) +{ + /* engine idiom (cf. zend_assign_to_variable): install the new value + * before releasing the old one — a __destruct re-reading the slot must + * observe the new value, not a freed zval */ + zval old; + ZVAL_COPY_VALUE(&old, z); + zval v = owned.take(); + ZVAL_COPY_VALUE(z, &v); + zval_ptr_dtor(&old); +} + +/* Owned zend_string: move-only RAII. NULL is the empty state, so a failed + * producer (e.g. pt_node_key on exception) can be adopted and tested. */ +class Str +{ + zend_string *s; + + explicit Str(zend_string *owned) : s(owned) {} + +public: + Str() : s(NULL) {} + Str(const Str &) = delete; + Str &operator=(const Str &) = delete; + Str(Str &&other) noexcept : s(other.s) { other.s = NULL; } + + Str &operator=(Str &&other) noexcept + { + if (this != &other) { + release(); + s = other.s; + other.s = NULL; + } + return *this; + } + + ~Str() { release(); } + + void release() + { + if (s != NULL) { + zend_string_release(s); + s = NULL; + } + } + + /* takes over an owned string (may be NULL) */ + static Str adopt(zend_string *owned) { return Str(owned); } + + /* addref-copy of a borrowed string */ + static Str copyOf(zend_string *borrowed) { return Str(zend_string_copy(borrowed)); } + + zend_string *get() const { return s; } + bool isNull() const { return s == NULL; } + + /* transfers ownership out */ + zend_string *take() + { + zend_string *out = s; + s = NULL; + return out; + } +}; + +/* + * Range-for iteration over a HashTable, handling both layouts: packed tables + * (PHP >= 8.2) store plain zvals in arPacked, mixed tables store Buckets in + * arData. Bucket's first member is its zval, so one byte-stride cursor walks + * both — the same dual layout ZEND_HASH_FOREACH compiles to. + */ +class ArrayEntry +{ + zval *slot; + zval *packedBase; /* start of arPacked for packed layout, NULL for Buckets */ + +public: + ArrayEntry(zval *s, zval *packedBase) : slot(s), packedBase(packedBase) {} + + bool hasStringKey() const { return packedBase == NULL && ((Bucket *) slot)->key != NULL; } + zend_string *stringKey() const { return ((Bucket *) slot)->key; } + /* string key, or NULL for integer keys — the pt_ht_* dual-key convention, + * exactly what ZEND_HASH_FOREACH_KEY_VAL's key variable holds */ + zend_string *stringKeyOrNull() const { return packedBase != NULL ? NULL : ((Bucket *) slot)->key; } + /* integer key; in packed layout that is the slot's position */ + zend_ulong indexKey() const { return packedBase != NULL ? (zend_ulong) (slot - packedBase) : ((Bucket *) slot)->h; } + Ref value() const { return Ref(slot); } +}; + +class ArrayIter +{ + char *cur; + char *end; + size_t stride; + zval *packedBase; /* NULL for Bucket layout */ + + void skipHoles() + { + while (cur != end && Z_TYPE_P((zval *) cur) == IS_UNDEF) { + cur += stride; + } + } + +public: + ArrayIter(char *c, char *e, size_t stride, bool packed) : cur(c), end(e), stride(stride), packedBase(packed ? (zval *) c : NULL) + { + skipHoles(); + } + + bool operator!=(const ArrayIter &other) const { return cur != other.cur; } + + void operator++() + { + cur += stride; + skipHoles(); + } + + ArrayEntry operator*() const { return ArrayEntry((zval *) cur, packedBase); } +}; + +/* Borrowed view of a bare HashTable (an HT zpp argument, a scratch table) — + * the same iteration ArrRef offers, without requiring a wrapping zval. */ +class TableRef +{ + HashTable *ht; + +public: + explicit TableRef(HashTable *h) : ht(h) {} + + HashTable *table() const { return ht; } + uint32_t size() const { return zend_hash_num_elements(ht); } + + ArrayIter begin() const + { + bool packed = HT_IS_PACKED(ht); + char *base = packed ? (char *) ht->arPacked : (char *) ht->arData; + size_t stride = packed ? sizeof(zval) : sizeof(Bucket); + return ArrayIter(base, base + stride * ht->nNumUsed, stride, packed); + } + + ArrayIter end() const + { + bool packed = HT_IS_PACKED(ht); + char *base = packed ? (char *) ht->arPacked : (char *) ht->arData; + size_t stride = packed ? sizeof(zval) : sizeof(Bucket); + char *e = base + stride * ht->nNumUsed; + return ArrayIter(e, e, stride, packed); + } +}; + +/* Borrowed view of a PHP array. */ +class ArrRef : public Ref +{ +public: + explicit ArrRef(zval *zvp) : Ref(zvp) {} + + HashTable *table() const { return Z_ARRVAL_P(z); } + uint32_t size() const { return zend_hash_num_elements(table()); } + + ArrayIter begin() const { return TableRef(table()).begin(); } + ArrayIter end() const { return TableRef(table()).end(); } + + /* symtable lookup: numeric strings behave like PHP array keys */ + Ref find(zend_string *key) const + { + zval *found = zend_symtable_find(table(), key); + return Ref(found); /* raw() == NULL when absent */ + } + + bool exists(zend_string *key) const { return zend_symtable_find(table(), key) != NULL; } + + /* $a[] = $value; separates a shared table first (writes through the slot) */ + void push(Ref borrowed) + { + SEPARATE_ARRAY(z); + Z_TRY_ADDREF_P(borrowed.raw()); + zend_hash_next_index_insert(table(), borrowed.raw()); + } +}; + +/* Owned PHP array under construction. */ +class Arr : public Val +{ +public: + Arr() = default; + + static Arr create(uint32_t sizeHint) + { + Arr a; + array_init_size(a.raw(), sizeHint); + return a; + } + + /* the shared immutable empty array — what a PHP [] literal provides */ + static Arr empty() + { + Arr a; + ZVAL_EMPTY_ARRAY(a.raw()); + return a; + } + + /* takes over a raw HashTable the caller owns; immutable tables (a PHP [] + * literal) are wrapped non-refcounted, like ZVAL_EMPTY_ARRAY */ + static Arr adoptTable(HashTable *owned) + { + Arr a; + ZVAL_ARR(a.raw(), owned); + if (GC_FLAGS(owned) & IS_ARRAY_IMMUTABLE) { + Z_TYPE_INFO_P(a.raw()) = IS_ARRAY; + } + return a; + } + + /* re-types an owned Val that is known to hold an array */ + static Arr adoptVal(Val ownedArray) + { + Arr a; + a.z = ownedArray.take(); + return a; + } + + /* addref-copy of a borrowed HashTable; immutable tables (a PHP [] + * literal) are wrapped non-refcounted so no addref ever touches them */ + static Arr copyOfTable(HashTable *borrowed) + { + Arr a; + ZVAL_ARR(a.raw(), borrowed); + if (!(GC_FLAGS(borrowed) & IS_ARRAY_IMMUTABLE)) { + GC_ADDREF(borrowed); + } else { + Z_TYPE_INFO_P(a.raw()) = IS_ARRAY; + } + return a; + } + + ArrRef arrRef() { return ArrRef(raw()); } + HashTable *table() { return Z_ARRVAL(z); } + + void separate() { SEPARATE_ARRAY(raw()); } + + /* $a[] = $value */ + void push(Ref borrowed) + { + separate(); + Z_TRY_ADDREF_P(borrowed.raw()); + zend_hash_next_index_insert(table(), borrowed.raw()); + } + + void push(Val owned) + { + separate(); + zval v = owned.take(); + zend_hash_next_index_insert(table(), &v); + } + + /* $a[$key] = $value (symtable semantics) */ + void set(zend_string *key, Val owned) + { + separate(); + zval v = owned.take(); + zend_symtable_update(table(), key, &v); + } + + /* $a['key'] = $value for a literal, never-numeric key */ + template + void set(const char (&key)[N], Val owned) + { + separate(); + zval v = owned.take(); + zend_hash_str_update(table(), key, N - 1, &v); + } +}; + +/* + * Stack-local scratch HashTable with no per-entry destructor: for tables of + * scalars or borrowed zvals that are never owned. Cheaper than a zval-wrapped + * Arr — the header lives on the C stack and nothing is refcounted. + */ +class ScratchTable +{ + HashTable ht; + +public: + explicit ScratchTable(uint32_t sizeHint) { zend_hash_init(&ht, sizeHint, NULL, NULL, 0); } + ScratchTable(const ScratchTable &) = delete; + ScratchTable &operator=(const ScratchTable &) = delete; + ~ScratchTable() { zend_hash_destroy(&ht); } + + HashTable *table() { return &ht; } + uint32_t size() const { return zend_hash_num_elements(&ht); } +}; + +/* Borrowed view of a zend object: property access via cached slots. */ +class ObjRef +{ +protected: + zend_object *obj; + +public: + explicit ObjRef(zend_object *o) : obj(o) {} + explicit ObjRef(zval *zvp) : obj(Z_OBJ_P(zvp)) {} + + zend_object *raw() const { return obj; } + zend_class_entry *ce() const { return obj->ce; } + uint32_t handle() const { return obj->handle; } + + bool instanceOf(zend_class_entry *classEntry) const { return instanceof_function(obj->ce, classEntry); } + + /* property at a known slot (OBJ_PROP_NUM) — the fast path for own classes */ + Ref propAt(uint32_t slot) const { return Ref(OBJ_PROP_NUM(obj, slot)); } + + /* property at a cached byte offset (OBJ_PROP; pt_instance_prop_offset) */ + Ref propAtOffset(uint32_t offset) const { return Ref(OBJ_PROP(obj, offset)); } + + /* property by name through the class's property table (pays a lookup) */ + Ref prop(const char *name, size_t len) const + { + int32_t offset = pt_instance_prop_offset(obj->ce, name, len); + if (offset < 0) { + return Ref(NULL); + } + zval *slot = OBJ_PROP(obj, (uint32_t) offset); + ZVAL_DEINDIRECT(slot); + return Ref(slot); + } + + void propAtWrite(uint32_t slot, Val owned) + { + /* new value in before the old one's dtor runs — see Ref::assign() */ + zval *p = OBJ_PROP_NUM(obj, slot); + zval old; + ZVAL_COPY_VALUE(&old, p); + zval v = owned.take(); + ZVAL_COPY_VALUE(p, &v); + zval_ptr_dtor(&old); + } + + /* $obj->$name = $value through the engine write path (magic methods and + * typed-property checks apply; may leave an exception pending) */ + void propWrite(zend_string *name, Ref value) + { + zend_update_property_ex(obj->ce, obj, name, value.raw()); + } +}; + +} // namespace zv + +#endif diff --git a/turbo-ext/stubs/CombinationsHelper.php b/turbo-ext/stubs/CombinationsHelper.php new file mode 100644 index 00000000000..4c78b4837e6 --- /dev/null +++ b/turbo-ext/stubs/CombinationsHelper.php @@ -0,0 +1,9 @@ +parse() + * over all of src/, interleaved rounds, reporting user CPU + peak memory. + */ + +$root = dirname(__DIR__, 2); +chdir($root); +require $root . '/vendor/autoload.php'; + +$files = []; +$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('src', FilesystemIterator::SKIP_DOTS)); +foreach ($it as $file) { + if ($file->getExtension() === 'php') { + $files[] = $file->getPathname(); + } +} +sort($files); +$codes = []; +$bytes = 0; +foreach ($files as $f) { + $codes[$f] = file_get_contents($f); + $bytes += strlen($codes[$f]); +} +printf("corpus: %d files, %.1f MB\n", count($codes), $bytes / 1048576); + +$lexer = new PhpParser\Lexer(); +$phpVersion = PhpParser\PhpVersion::fromString('8.5'); + +function userTime(): float +{ + $r = getrusage(); + return $r['ru_utime.tv_sec'] + $r['ru_utime.tv_usec'] / 1e6; +} + +$rounds = 6; // interleaved: odd = native, even = php +$results = ['native' => [], 'php' => []]; + +for ($round = 0; $round < $rounds; $round++) { + $mode = $round % 2 === 0 ? 'native' : 'php'; + $parser = new PhpParser\Parser\Php8($lexer, $phpVersion); + gc_collect_cycles(); + $memBefore = memory_get_usage(); + $t0 = userTime(); + $nodes = 0; + foreach ($codes as $code) { + $handler = new PhpParser\ErrorHandler\Collecting(); + if ($mode === 'native') { + $ast = PHPStanTurbo\ParserRunner::parse($parser, $code, $handler); + } else { + $ast = $parser->parse($code, $handler); + } + $nodes += count($ast); + unset($ast); + } + $elapsed = userTime() - $t0; + $results[$mode][] = $elapsed; + printf("round %d %-6s %6.3fs user transient-mem %5.1f MB peak %5.1f MB\n", + $round, $mode, $elapsed, (memory_get_usage() - $memBefore) / 1048576, memory_get_peak_usage() / 1048576); +} + +$avg = static fn (array $a) => array_sum($a) / count($a); +// drop the first round of each mode (warmup: class resolution, table extraction) +$nat = array_slice($results['native'], 1); +$php = array_slice($results['php'], 1); +printf("\nnative mean %.3fs php mean %.3fs speedup %.2fx (%.1f%% less time)\n", + $avg($nat), $avg($php), $avg($php) / $avg($nat), 100 * (1 - $avg($nat) / $avg($php))); + +// retained-AST memory comparison: keep all ASTs of one mode in memory +foreach (['native', 'php'] as $mode) { + $parser = new PhpParser\Parser\Php8($lexer, $phpVersion); + gc_collect_cycles(); + $before = memory_get_usage(); + $asts = []; + foreach ($codes as $f => $code) { + $handler = new PhpParser\ErrorHandler\Collecting(); + $asts[$f] = $mode === 'native' + ? PHPStanTurbo\ParserRunner::parse($parser, $code, $handler) + : $parser->parse($code, $handler); + } + gc_collect_cycles(); + printf("retained ASTs (%s): %.1f MB\n", $mode, (memory_get_usage() - $before) / 1048576); + unset($asts); + gc_collect_cycles(); +} diff --git a/turbo-ext/tests/parser-corpus.php b/turbo-ext/tests/parser-corpus.php new file mode 100644 index 00000000000..ced4af7ed9a --- /dev/null +++ b/turbo-ext/tests/parser-corpus.php @@ -0,0 +1,148 @@ +parse() (PHP), and requires byte-identical serialized ASTs, + * identical collected errors, and identical token counts. + * + * Run with the extension loaded and vendor/ installed: + * php -d extension=$PWD/turbo-ext/phpstan_turbo.so turbo-ext/tests/parser-corpus.php [maxFiles] + * + * The enabler is NOT run; PHPStanTurbo\ParserRunner is called directly. + */ + +$root = dirname(__DIR__, 2); +chdir($root); + +if (!extension_loaded('phpstan_turbo')) { + fwrite(STDERR, "the phpstan_turbo extension is not loaded\n"); + exit(1); +} + +require $root . '/vendor/autoload.php'; + +$maxFiles = isset($argv[1]) ? (int) $argv[1] : PHP_INT_MAX; + +$dirs = [ + 'src', + 'tests/PHPStan', + 'vendor/nikic/php-parser/lib', + 'vendor/phpstan/phpdoc-parser/src', + 'vendor/symfony', + 'stubs', + // regression fixtures for behavior only malformed/exotic input exercises + // (dropped T_BAD_CHARACTER, aborting escape-sequence errors, the + // first-class-callable exit() construction-plan poisoning) + 'turbo-ext/tests/parser-fixtures', +]; + +$files = []; +foreach ($dirs as $dir) { + if (!is_dir($dir)) { + continue; + } + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); + foreach ($it as $file) { + if ($file->getExtension() === 'php') { + $files[] = $file->getPathname(); + } + } +} +sort($files); +$files = array_slice($files, 0, $maxFiles); + +$lexer = new PhpParser\Lexer(); +$phpVersion = PhpParser\PhpVersion::fromString('8.5'); +$parserForNative = new PhpParser\Parser\Php8($lexer, $phpVersion); +$parserForPhp = new PhpParser\Parser\Php8($lexer, $phpVersion); + +function summarizeErrors(PhpParser\ErrorHandler\Collecting $handler): string +{ + $out = []; + foreach ($handler->getErrors() as $error) { + $out[] = $error->getRawMessage() . '|' . var_export($error->getAttributes(), true); + } + + return implode("\n", $out); +} + +$checked = 0; +$failed = 0; +$parseErrors = 0; +$firstDiffs = []; + +foreach ($files as $file) { + $code = file_get_contents($file); + if ($code === false) { + continue; + } + + $nativeHandler = new PhpParser\ErrorHandler\Collecting(); + $phpHandler = new PhpParser\ErrorHandler\Collecting(); + + $nativeThrew = null; + $phpThrew = null; + $nativeAst = null; + $phpAst = null; + try { + $nativeAst = PHPStanTurbo\ParserRunner::parse($parserForNative, $code, $nativeHandler); + } catch (Throwable $e) { + $nativeThrew = get_class($e) . ': ' . $e->getMessage(); + } + $nativeTokens = count($parserForNative->getTokens()); + try { + $phpAst = $parserForPhp->parse($code, $phpHandler); + } catch (Throwable $e) { + $phpThrew = get_class($e) . ': ' . $e->getMessage(); + } + $phpTokens = count($parserForPhp->getTokens()); + + $checked++; + $problems = []; + if ($nativeThrew !== $phpThrew) { + $problems[] = sprintf('throw mismatch: native=%s php=%s', $nativeThrew ?? '-', $phpThrew ?? '-'); + } + if ($nativeTokens !== $phpTokens) { + $problems[] = sprintf('token count mismatch: native=%d php=%d', $nativeTokens, $phpTokens); + } + $nativeErrors = summarizeErrors($nativeHandler); + $phpErrors = summarizeErrors($phpHandler); + if ($nativeErrors !== $phpErrors) { + $problems[] = sprintf("errors mismatch:\n--- native ---\n%s\n--- php ---\n%s", $nativeErrors, $phpErrors); + } + if ($phpErrors !== '') { + $parseErrors++; + } + if ($problems === []) { + $nativeSer = $nativeAst === null ? 'NULL' : serialize($nativeAst); + $phpSer = $phpAst === null ? 'NULL' : serialize($phpAst); + if ($nativeSer !== $phpSer) { + // find the first differing offset for the report + $len = min(strlen($nativeSer), strlen($phpSer)); + $at = 0; + while ($at < $len && $nativeSer[$at] === $phpSer[$at]) { + $at++; + } + $problems[] = sprintf( + "AST mismatch at byte %d:\n native: …%s…\n php: …%s…", + $at, + substr($nativeSer, max(0, $at - 60), 160), + substr($phpSer, max(0, $at - 60), 160), + ); + } + } + + if ($problems !== []) { + $failed++; + if (count($firstDiffs) < 10) { + $firstDiffs[] = sprintf("=== %s ===\n%s", $file, implode("\n", $problems)); + } + } +} + +foreach ($firstDiffs as $diff) { + echo $diff, "\n\n"; +} +printf("corpus: %d files checked, %d with parse errors (identical both sides counts as pass), %d FAILED\n", $checked, $parseErrors, $failed); +exit($failed > 0 ? 1 : 0); diff --git a/turbo-ext/tests/parser-fixtures/bad-character-token.php b/turbo-ext/tests/parser-fixtures/bad-character-token.php new file mode 100644 index 00000000000..9b0000c7e43 --- /dev/null +++ b/turbo-ext/tests/parser-fixtures/bad-character-token.php @@ -0,0 +1,4 @@ + $nativeToTwin + */ +function normalizeType(?ReflectionType $type, array $nativeToTwin, string $selfClass): string +{ + $s = strtolower((string) $type); + $s = preg_replace('~(^|\||&|\?)(self|static)($|\||&)~', '$1' . $selfClass . '$3', $s); + + return strtr($s, $nativeToTwin); +} + +$nativeToTwin = []; +foreach ($manifest as $twinClass => $entry) { + $nativeToTwin[strtolower('PHPStanTurbo\\' . basename($entry['cpp'], '.cpp'))] = strtolower($twinClass); +} + +function visibility(ReflectionMethod $m): string +{ + return $m->isPrivate() ? 'private' : ($m->isProtected() ? 'protected' : 'public'); +} + +$failed = false; +$compared = 0; + +foreach ($manifest as $twinClass => $entry) { + $nativeClass = 'PHPStanTurbo\\' . basename($entry['cpp'], '.cpp'); + $twin = new ReflectionClass($twinClass); + $native = new ReflectionClass($nativeClass); + + $problems = []; + + // the manifest must point at the file the class actually lives in + // (bin/side-by-side.php parses that file's source as the PHP side) + $actualFile = substr(realpath($twin->getFileName()), strlen(realpath($root)) + 1); + if ($actualFile !== $entry['php']) { + $problems[] = sprintf('lives in %s, but the manifest says %s — regenerate with bin/side-by-side.php --update-manifest', $actualFile, $entry['php']); + } + if (($entry['vendored'] ?? false) !== str_starts_with($actualFile, 'vendor/')) { + $problems[] = sprintf('the manifest "vendored" flag does not match the class location %s', $actualFile); + } + foreach ($native->getMethods() as $nativeMethod) { + $name = $nativeMethod->getName(); + if (!$twin->hasMethod($name)) { + continue; // orphan — side-by-side.php --check reports it + } + $twinMethod = $twin->getMethod($name); + $compared++; + + if (visibility($nativeMethod) !== visibility($twinMethod)) { + $problems[] = sprintf('%s(): %s natively, %s in PHP', $name, visibility($nativeMethod), visibility($twinMethod)); + } + if ($nativeMethod->isStatic() !== $twinMethod->isStatic()) { + $problems[] = sprintf('%s(): static-ness differs', $name); + } + + $nativeParams = $nativeMethod->getParameters(); + $twinParams = $twinMethod->getParameters(); + if (count($nativeParams) !== count($twinParams) + || $nativeMethod->getNumberOfRequiredParameters() !== $twinMethod->getNumberOfRequiredParameters() + ) { + $problems[] = sprintf( + '%s(): %d params (%d required) natively, %d (%d required) in PHP', + $name, + count($nativeParams), + $nativeMethod->getNumberOfRequiredParameters(), + count($twinParams), + $twinMethod->getNumberOfRequiredParameters(), + ); + } else { + foreach ($nativeParams as $i => $nativeParam) { + $twinParam = $twinParams[$i]; + if ($nativeParam->getName() !== $twinParam->getName()) { + $problems[] = sprintf('%s(): parameter #%d is $%s natively, $%s in PHP — breaks named arguments', $name, $i + 1, $nativeParam->getName(), $twinParam->getName()); + } + if ($nativeParam->isPassedByReference() !== $twinParam->isPassedByReference()) { + $problems[] = sprintf('%s($%s): by-ref differs', $name, $twinParam->getName()); + } + if ($nativeParam->isVariadic() !== $twinParam->isVariadic()) { + $problems[] = sprintf('%s($%s): variadic differs', $name, $twinParam->getName()); + } + if (!isErased($nativeParam->getType())) { + $nativeType = normalizeType($nativeParam->getType(), $nativeToTwin, strtolower($twinClass)); + $twinType = normalizeType($twinParam->getType(), $nativeToTwin, strtolower($twinClass)); + if ($nativeType !== $twinType) { + $problems[] = sprintf('%s($%s): type "%s" natively, "%s" in PHP', $name, $twinParam->getName(), $nativeType, $twinType); + } + } + } + } + + $nativeReturnType = $nativeMethod->getReturnType() ?? $nativeMethod->getTentativeReturnType(); + if (!isErased($nativeReturnType)) { + $nativeReturn = normalizeType($nativeReturnType, $nativeToTwin, strtolower($twinClass)); + $twinReturn = normalizeType($twinMethod->getReturnType() ?? $twinMethod->getTentativeReturnType(), $nativeToTwin, strtolower($twinClass)); + if ($nativeReturn !== $twinReturn) { + $problems[] = sprintf('%s(): returns "%s" natively, "%s" in PHP', $name, $nativeReturn, $twinReturn); + } + } + } + + if ($problems === []) { + printf("✓ %s\n", $twinClass); + continue; + } + $failed = true; + foreach ($problems as $problem) { + printf("✗ %s::%s\n", $twinClass, $problem); + } +} + +printf($failed ? "FAILED\n" : "OK (%d methods compared)\n", $compared); +exit($failed ? 1 : 0); diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php new file mode 100644 index 00000000000..a6763717e96 --- /dev/null +++ b/turbo-ext/tests/smoke.php @@ -0,0 +1,282 @@ + \PHPStan\Type\TypeCombinator::class, + 'type' => \PHPStan\Type\Type::class, + 'recursionGuard' => \PHPStan\Type\RecursionGuard::class, + 'booleanType' => \PHPStan\Type\BooleanType::class, + 'constantBooleanType' => \PHPStan\Type\Constant\ConstantBooleanType::class, + 'shouldNotHappenException' => \PHPStan\ShouldNotHappenException::class, + 'verbosityLevel' => \PHPStan\Type\VerbosityLevel::class, + 'variable' => \PhpParser\Node\Expr\Variable::class, + 'funcCall' => \PhpParser\Node\Expr\FuncCall::class, + 'virtualNode' => \PHPStan\Node\VirtualNode::class, + 'node' => \PhpParser\Node::class, + 'name' => \PhpParser\Node\Name::class, + 'expr' => \PhpParser\Node\Expr::class, + 'propertyFetch' => \PhpParser\Node\Expr\PropertyFetch::class, + 'intertwinedVariableByReferenceWithExpr' => \PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr::class, + 'arrayDimFetch' => \PhpParser\Node\Expr\ArrayDimFetch::class, + 'methodCall' => \PhpParser\Node\Expr\MethodCall::class, + 'functionLike' => \PhpParser\Node\FunctionLike::class, +]); + +// ---- TrinaryLogic ---- +$pYes = TrinaryLogic::createYes(); +$pNo = TrinaryLogic::createNo(); +$pMaybe = TrinaryLogic::createMaybe(); +$nYes = \PHPStanTurbo\TrinaryLogic::createYes(); +$nNo = \PHPStanTurbo\TrinaryLogic::createNo(); +$nMaybe = \PHPStanTurbo\TrinaryLogic::createMaybe(); + +$pAll = ['yes' => $pYes, 'no' => $pNo, 'maybe' => $pMaybe]; +$nAll = ['yes' => $nYes, 'no' => $nNo, 'maybe' => $nMaybe]; + +check($nYes === \PHPStanTurbo\TrinaryLogic::createYes(), 'createYes identity'); +check($nNo === \PHPStanTurbo\TrinaryLogic::createFromBoolean(false), 'createFromBoolean(false) identity'); +check($nYes === \PHPStanTurbo\TrinaryLogic::createFromBoolean(true), 'createFromBoolean(true) identity'); + +foreach (['yes', 'no', 'maybe'] as $k) { + check($pAll[$k]->yes() === $nAll[$k]->yes(), "$k yes()"); + check($pAll[$k]->no() === $nAll[$k]->no(), "$k no()"); + check($pAll[$k]->maybe() === $nAll[$k]->maybe(), "$k maybe()"); + check($pAll[$k]->describe() === $nAll[$k]->describe(), "$k describe()"); + check(get_class($pAll[$k]->toBooleanType()) === get_class($nAll[$k]->toBooleanType()), "$k toBooleanType() class"); + check($pAll[$k]->toBooleanType()->describe(\PHPStan\Type\VerbosityLevel::precise()) === $nAll[$k]->toBooleanType()->describe(\PHPStan\Type\VerbosityLevel::precise()), "$k toBooleanType() describe"); + foreach (['yes', 'no', 'maybe'] as $j) { + check($pAll[$k]->and($pAll[$j])->describe() === $nAll[$k]->and($nAll[$j])->describe(), "$k and $j"); + check($pAll[$k]->or($pAll[$j])->describe() === $nAll[$k]->or($nAll[$j])->describe(), "$k or $j"); + check($pAll[$k]->equals($pAll[$j]) === $nAll[$k]->equals($nAll[$j]), "$k equals $j"); + $pc = $pAll[$k]->compareTo($pAll[$j]); + $nc = $nAll[$k]->compareTo($nAll[$j]); + check(($pc === null) === ($nc === null) && ($pc === null || $pc->describe() === $nc->describe()), "$k compareTo $j"); + foreach (['yes', 'no', 'maybe'] as $m) { + check( + $pAll[$k]->and($pAll[$j], $pAll[$m])->describe() === $nAll[$k]->and($nAll[$j], $nAll[$m])->describe(), + "$k and($j,$m)", + ); + check( + TrinaryLogic::extremeIdentity($pAll[$k], $pAll[$j], $pAll[$m])->describe() === \PHPStanTurbo\TrinaryLogic::extremeIdentity($nAll[$k], $nAll[$j], $nAll[$m])->describe(), + "extremeIdentity($k,$j,$m)", + ); + check( + TrinaryLogic::maxMin($pAll[$k], $pAll[$j], $pAll[$m])->describe() === \PHPStanTurbo\TrinaryLogic::maxMin($nAll[$k], $nAll[$j], $nAll[$m])->describe(), + "maxMin($k,$j,$m)", + ); + } + } + check($pAll[$k]->negate()->describe() === $nAll[$k]->negate()->describe(), "$k negate()"); + check($pAll[$k]->and()->describe() === $nAll[$k]->and()->describe(), "$k and() no args"); + check($pAll[$k]->or()->describe() === $nAll[$k]->or()->describe(), "$k or() no args"); +} + +// lazy* +$keys = ['yes', 'no', 'maybe']; +foreach ($keys as $k) { + foreach ([['yes', 'maybe'], ['no', 'no'], ['maybe', 'yes', 'no'], []] as $items) { + $pcb = static fn (string $s) => $GLOBALS['pAll'][$s] ?? TrinaryLogic::createYes(); + $ncb = static fn (string $s) => $GLOBALS['nAll'][$s] ?? \PHPStanTurbo\TrinaryLogic::createYes(); + $GLOBALS['pAll'] = $pAll; + $GLOBALS['nAll'] = $nAll; + check( + $pAll[$k]->lazyAnd($items, $pcb)->describe() === $nAll[$k]->lazyAnd($items, $ncb)->describe(), + "$k lazyAnd " . implode(',', $items), + ); + check( + $pAll[$k]->lazyOr($items, $pcb)->describe() === $nAll[$k]->lazyOr($items, $ncb)->describe(), + "$k lazyOr " . implode(',', $items), + ); + } +} +foreach ([['yes'], ['yes', 'yes'], ['yes', 'maybe'], ['no', 'no'], ['maybe', 'no', 'yes']] as $items) { + $pcb = static fn (string $s) => $GLOBALS['pAll'][$s]; + $ncb = static fn (string $s) => $GLOBALS['nAll'][$s]; + check( + TrinaryLogic::lazyExtremeIdentity($items, $pcb)->describe() === \PHPStanTurbo\TrinaryLogic::lazyExtremeIdentity($items, $ncb)->describe(), + 'lazyExtremeIdentity ' . implode(',', $items), + ); + check( + TrinaryLogic::lazyMaxMin($items, $pcb)->describe() === \PHPStanTurbo\TrinaryLogic::lazyMaxMin($items, $ncb)->describe(), + 'lazyMaxMin ' . implode(',', $items), + ); +} + +// empty extremeIdentity/maxMin must throw ShouldNotHappenException +foreach (['extremeIdentity', 'maxMin'] as $m) { + try { + \PHPStanTurbo\TrinaryLogic::$m(); + check(false, "$m() empty should throw"); + } catch (\PHPStan\ShouldNotHappenException) { + // ok + } +} + +// lazyMaxMin([]) does NOT throw — it returns Yes ($min starts at YES), unlike +// its non-lazy sibling +$neverCalled = static function ($o) { + throw new \LogicException('callback must not run for an empty array'); +}; +check( + TrinaryLogic::lazyMaxMin([], $neverCalled)->describe() === \PHPStanTurbo\TrinaryLogic::lazyMaxMin([], $neverCalled)->describe() + && \PHPStanTurbo\TrinaryLogic::lazyMaxMin([], $neverCalled)->yes(), + 'lazyMaxMin([]) returns Yes', +); + +// ---- CombinationsHelper ---- +$cases = [ + [], + [[1, 2, 3]], + [[1, 2], ['a', 'b', 'c']], + [[1], [2], [3]], + [[1, 2], [], [3]], + [['x' => 1, 'y' => 2], [true, false]], + [[1.5, 'str', null], [[], [1]], [7]], +]; +foreach ($cases as $i => $case) { + $php = []; + foreach (\PHPStan\Internal\CombinationsHelper::combinations($case) as $c) { + $php[] = $c; + } + $native = \PHPStanTurbo\CombinationsHelper::combinations($case); + if (!is_array($native)) { + $native = iterator_to_array($native, false); + } + check($php === $native, "combinations case $i: " . json_encode($php) . ' vs ' . json_encode($native)); +} + +// ---- ExpressionTypeHolder ---- +$expr1 = new \PhpParser\Node\Expr\Variable('a'); +$expr2 = new \PhpParser\Node\Expr\Variable('b'); +$int = new \PHPStan\Type\IntegerType(); +$string = new \PHPStan\Type\StringType(); +$int2 = new \PHPStan\Type\IntegerType(); + +$pH = static fn ($expr, $type, $c) => new \PHPStan\Analyser\ExpressionTypeHolder($expr, $type, $c); +$nH = static fn ($expr, $type, $c) => new \PHPStanTurbo\ExpressionTypeHolder($expr, $type, $c); + +$combos = [ + [$expr1, $int, 'yes'], + [$expr1, $int2, 'maybe'], + [$expr1, $string, 'no'], + [$expr2, $string, 'yes'], +]; +foreach ($combos as [$e1, $t1, $c1]) { + foreach ($combos as [$e2, $t2, $c2]) { + $p1 = $pH($e1, $t1, $pAll[$c1]); + $p2 = $pH($e2, $t2, $pAll[$c2]); + $n1 = $nH($e1, $t1, $nAll[$c1]); + $n2 = $nH($e2, $t2, $nAll[$c2]); + check($p1->equals($p2) === $n1->equals($n2), "ETH equals $c1/$c2 " . $t1->describe(\PHPStan\Type\VerbosityLevel::precise()) . '/' . $t2->describe(\PHPStan\Type\VerbosityLevel::precise())); + check($p1->equalTypes($p2) === $n1->equalTypes($n2), "ETH equalTypes"); + $pa = $p1->and($p2); + $na = $n1->and($n2); + check($pa->getCertainty()->describe() === $na->getCertainty()->describe(), "ETH and certainty $c1/$c2"); + check($pa->getType()->describe(\PHPStan\Type\VerbosityLevel::precise()) === $na->getType()->describe(\PHPStan\Type\VerbosityLevel::precise()), "ETH and type"); + check($pa->getExpr() === $na->getExpr() || $pa->getExpr()->name === $na->getExpr()->name, "ETH and expr"); + } +} +// identity semantics of and(): same type object, certainty yes+yes -> $this +$n1 = $nH($expr1, $int, $nYes); +$n2 = $nH($expr2, $int, $nYes); +check($n1->and($n2) === $n1, 'ETH and identity (same type, yes+yes)'); +$nMaybeH = $nH($expr1, $int, $nMaybe); +check($nMaybeH->and($n2) === $nMaybeH, 'ETH and identity (maybe this)'); +$nNoH = $nH($expr1, $int, $nNo); +check($nNoH->and($n2) === $n2, 'ETH and returns other (no this)'); +// createYes / createMaybe +check(\PHPStanTurbo\ExpressionTypeHolder::createYes($expr1, $int)->getCertainty()->yes(), 'ETH createYes'); +check(\PHPStanTurbo\ExpressionTypeHolder::createMaybe($expr1, $int)->getCertainty()->maybe(), 'ETH createMaybe'); +check(\PHPStanTurbo\ExpressionTypeHolder::createYes($expr1, $int)->getType() === $int, 'ETH createYes type identity'); + +// ---- ConditionalExpressionHolder ---- +$pCEH = new \PHPStan\Analyser\ConditionalExpressionHolder( + ['$a' => $pH($expr1, $int, $pYes), '$b' => $pH($expr2, $string, $pMaybe)], + $pH($expr2, $string, $pNo), +); +$nCEH = new \PHPStanTurbo\ConditionalExpressionHolder( + ['$a' => $nH($expr1, $int, $nYes), '$b' => $nH($expr2, $string, $nMaybe)], + $nH($expr2, $string, $nNo), +); +check($pCEH->getKey() === $nCEH->getKey(), 'CEH getKey: ' . $pCEH->getKey() . ' vs ' . $nCEH->getKey()); +check(count($nCEH->getConditionExpressionTypeHolders()) === 2, 'CEH holders count'); +check($nCEH->getTypeHolder()->getCertainty()->no(), 'CEH typeHolder'); +try { + new \PHPStanTurbo\ConditionalExpressionHolder([], $nH($expr1, $int, $nYes)); + check(false, 'CEH empty should throw'); +} catch (\PHPStan\ShouldNotHappenException) { +} + +// ---- TypeCombinatorCache ---- +// The native class memoizes on a structural key of the arguments and calls back into +// TypeCombinator::doUnion() and friends on a miss. TypeCombinator itself is unshadowed +// here (the enabler never ran), so it is the unmemoized reference implementation. +$cacheLevel = \PHPStan\Type\VerbosityLevel::cache(); +$describe = static fn (\PHPStan\Type\Type $t): string => $t->describe($cacheLevel); + +$intT = new \PHPStan\Type\IntegerType(); +$stringT = new \PHPStan\Type\StringType(); +$nullT = new \PHPStan\Type\NullType(); +$oneT = new \PHPStan\Type\Constant\ConstantIntegerType(1); +$tenT = new \PHPStan\Type\Constant\ConstantIntegerType(10); +$arrayT = new \PHPStan\Type\ArrayType(new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType()); +$nonEmpty = new \PHPStan\Type\Accessory\NonEmptyArrayType(); + +$unions = [ + [$intT, $stringT], + [$oneT, $tenT, $nullT], + [new \PHPStan\Type\UnionType([$oneT, $tenT]), $nullT], +]; +foreach ($unions as $i => $args) { + $native = \PHPStanTurbo\TypeCombinatorCache::union(...$args); + $php = \PHPStan\Type\TypeCombinator::union(...$args); + check($describe($native) === $describe($php), "TCC union #$i: {$describe($native)} vs {$describe($php)}"); +} + +$native = \PHPStanTurbo\TypeCombinatorCache::intersect($arrayT, $nonEmpty); +$php = \PHPStan\Type\TypeCombinator::intersect($arrayT, $nonEmpty); +check($describe($native) === $describe($php), 'TCC intersect: ' . $describe($native) . ' vs ' . $describe($php)); + +$nullable = \PHPStan\Type\TypeCombinator::union($intT, $nullT); +$native = \PHPStanTurbo\TypeCombinatorCache::remove($nullable, $nullT); +$php = \PHPStan\Type\TypeCombinator::remove($nullable, $nullT); +check($describe($native) === $describe($php), 'TCC remove: ' . $describe($native) . ' vs ' . $describe($php)); + +// a repeated call must hit the memo and hand back the very same instance +$first = \PHPStanTurbo\TypeCombinatorCache::union($intT, $stringT); +$second = \PHPStanTurbo\TypeCombinatorCache::union(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()); +check($first === $second, 'TCC memo hit on structurally equal arguments'); + +// explicit and implicit mixed are different values and must not share a memo entry +$explicit = \PHPStanTurbo\TypeCombinatorCache::union(new \PHPStan\Type\MixedType(true), $intT); +$implicit = \PHPStanTurbo\TypeCombinatorCache::union(new \PHPStan\Type\MixedType(false), $intT); +check($describe($explicit) !== $describe($implicit), 'TCC keeps explicit/implicit mixed apart'); + +\PHPStanTurbo\TypeCombinatorCache::clearCache(); +$afterClear = \PHPStanTurbo\TypeCombinatorCache::union($intT, $stringT); +check($describe($afterClear) === $describe($first), 'TCC clearCache keeps results correct'); +check($afterClear !== $first, 'TCC clearCache actually drops entries'); + +echo $failures === 0 ? "ALL OK\n" : "$failures FAILURES\n"; +exit($failures === 0 ? 0 : 1);