diff --git a/build/gen_stub.php b/build/gen_stub.php index a3dd14a56234..956d5cec2582 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -54,7 +54,7 @@ function processDirectory(string $dir, Context $context): array { ); foreach ($it as $file) { $pathName = $file->getPathName(); - if (preg_match('/\.stub\.php$/', $pathName)) { + if (substr($pathName, -9) === '.stub.php') { $pathNames[] = $pathName; } } @@ -84,7 +84,7 @@ function processStubFile(string $stubFile, Context $context, bool $includeOnly = $legacyFile = "{$stubFilenameWithoutExtension}_legacy_arginfo.h"; $stubCode = file_get_contents($stubFile); - $stubHash = computeStubHash($stubCode); + $stubHash = sha1(str_replace("\r\n", "\n", $stubCode)); $oldStubHash = extractStubHash($arginfoFile); if ($stubHash === $oldStubHash && !$context->forceParse) { /* Stub file did not change, do not regenerate. */ @@ -153,10 +153,6 @@ function processStubFile(string $stubFile, Context $context, bool $includeOnly = } } -function computeStubHash(string $stubCode): string { - return sha1(str_replace("\r\n", "\n", $stubCode)); -} - function extractStubHash(string $arginfoFile): ?string { if (!file_exists($arginfoFile)) { return null; @@ -361,79 +357,51 @@ public function isMixed(): bool { return $this->isBuiltin && $this->name === 'mixed'; } - public function toTypeCode(): string { - assert($this->isBuiltin); - switch ($this->name) { - case "bool": - return "_IS_BOOL"; - case "int": - return "IS_LONG"; - case "float": - return "IS_DOUBLE"; - case "string": - return "IS_STRING"; - case "array": - return "IS_ARRAY"; - case "object": - return "IS_OBJECT"; - case "void": - return "IS_VOID"; - case "callable": - return "IS_CALLABLE"; - case "mixed": - return "IS_MIXED"; - case "static": - return "IS_STATIC"; - case "never": - return "IS_NEVER"; - case "null": - return "IS_NULL"; - case "false": - return "IS_FALSE"; - case "true": - return "IS_TRUE"; - default: - throw new Exception("Not implemented: $this->name"); - } - } - - public function toTypeMask(): string { + private function toTypeInfo(): array { assert($this->isBuiltin); switch ($this->name) { case "null": - return "MAY_BE_NULL"; + return ["IS_NULL", "MAY_BE_NULL"]; case "false": - return "MAY_BE_FALSE"; + return ["IS_FALSE", "MAY_BE_FALSE"]; case "true": - return "MAY_BE_TRUE"; + return ["IS_TRUE", "MAY_BE_TRUE"]; case "bool": - return "MAY_BE_BOOL"; + return ["_IS_BOOL", "MAY_BE_BOOL"]; case "int": - return "MAY_BE_LONG"; + return ["IS_LONG", "MAY_BE_LONG"]; case "float": - return "MAY_BE_DOUBLE"; + return ["IS_DOUBLE", "MAY_BE_DOUBLE"]; case "string": - return "MAY_BE_STRING"; + return ["IS_STRING", "MAY_BE_STRING"]; case "array": - return "MAY_BE_ARRAY"; + return ["IS_ARRAY", "MAY_BE_ARRAY"]; case "object": - return "MAY_BE_OBJECT"; + return ["IS_OBJECT", "MAY_BE_OBJECT"]; case "callable": - return "MAY_BE_CALLABLE"; + return ["IS_CALLABLE", "MAY_BE_CALLABLE"]; case "mixed": - return "MAY_BE_ANY"; + return ["IS_MIXED", "MAY_BE_ANY"]; case "void": - return "MAY_BE_VOID"; + return ["IS_VOID", "MAY_BE_VOID"]; case "static": - return "MAY_BE_STATIC"; + return ["IS_STATIC", "MAY_BE_STATIC"]; case "never": - return "MAY_BE_NEVER"; + return ["IS_NEVER", "MAY_BE_NEVER"]; default: throw new Exception("Not implemented: $this->name"); } } + public function toTypeCode(): string { + return $this->toTypeInfo()[0]; + } + + public function toTypeMask(): string { + return $this->toTypeInfo()[1]; + } + public function toOptimizerTypeMaskForArrayKey(): string { assert($this->isBuiltin); @@ -898,13 +866,6 @@ public function getDeclarationName(): string; abstract class AbstractConstName implements VariableLikeName { - public function equals(AbstractConstName $const): bool - { - return $this->__toString() === $const->__toString(); - } - - abstract public function isClassConst(): bool; - public function isUnknown(): bool { return strtolower($this->__toString()) === "unknown"; @@ -922,11 +883,6 @@ public function __construct(?Name $namespace, string $const) $this->const = $const; } - public function isClassConst(): bool - { - return false; - } - public function isUnknown(): bool { $name = $this->__toString(); @@ -957,11 +913,6 @@ public function __construct(Name $class, string $const) $this->const = $const; } - public function isClassConst(): bool - { - return true; - } - public function __toString(): string { return $this->class->toString() . "::" . $this->const; @@ -1000,7 +951,6 @@ public function getArgInfoName(): string; public function getMethodSynopsisFilename(): string; public function getNameForAttributes(): string; public function __toString(): string; - public function isMethod(): bool; public function isConstructor(): bool; public function isDestructor(): bool; } @@ -1063,10 +1013,6 @@ public function __toString(): string { return $this->name->toString(); } - public function isMethod(): bool { - return false; - } - public function isConstructor(): bool { return false; } @@ -1112,10 +1058,6 @@ public function __toString(): string { return "$this->className::$this->methodName"; } - public function isMethod(): bool { - return true; - } - public function isConstructor(): bool { return $this->methodName === "__construct"; } @@ -1247,6 +1189,121 @@ private function beginArgInfoCompatible(string $funcInfoName, int $minArgs): str } } +class VersionFlags { + + /** + * Keys are the PHP versions, values are arrays of flags + */ + private array $flagsByVersion; + + public function __construct(array $baseFlags) { + $this->flagsByVersion = []; + foreach (ALL_PHP_VERSION_IDS as $version) { + $this->flagsByVersion[$version] = $baseFlags; + } + } + + public function addForVersionsAbove(string $flag, int $minimumVersionId) { + $write = false; + + foreach (ALL_PHP_VERSION_IDS as $version) { + if ($version === $minimumVersionId || $write === true) { + $this->flagsByVersion[$version][] = $flag; + $write = true; + } + } + } + + public function isEmpty(): bool { + foreach (ALL_PHP_VERSION_IDS as $version) { + if ( $this->flagsByVersion[$version] !== []) { + return false; + } + } + return true; + } + + public function generateVersionDependentFlagCode( + string $codeTemplate, + ?int $phpVersionIdMinimumCompatibility, + ?int $phpVersionIdMaxCompatibility = null + ): string { + $flagsByPhpVersions = $this->flagsByVersion; + $phpVersions = ALL_PHP_VERSION_IDS; + sort($phpVersions); + $currentPhpVersion = end($phpVersions); + + // No version compatibility is needed + if ($phpVersionIdMinimumCompatibility === null) { + if (empty($flagsByPhpVersions[$currentPhpVersion])) { + return ''; + } + return sprintf($codeTemplate, implode("|", $flagsByPhpVersions[$currentPhpVersion])); + } + + ksort($flagsByPhpVersions); + if ($phpVersionIdMinimumCompatibility !== null) { + // Remove flags which depend on a PHP version below the minimally supported one + $index = array_search($phpVersionIdMinimumCompatibility, array_keys($flagsByPhpVersions)); + if ($index === false) { + throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMinimumCompatibility\""); + } + $flagsByPhpVersions = array_slice($flagsByPhpVersions, $index, null, true); + } + if ($phpVersionIdMaxCompatibility !== null) { + // Remove flags which depend on a PHP version above the maximally supported one + $index = array_search($phpVersionIdMaxCompatibility, array_keys($flagsByPhpVersions)); + if ($index === false) { + throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMaxCompatibility\""); + } + $flagsByPhpVersions = array_slice($flagsByPhpVersions, 0, $index, true); + } + + // Remove version-specific flags which don't differ from the previous one + // Also populate '0' values + $previousVersionId = null; + foreach ($flagsByPhpVersions as $versionId => $versionFlags) { + if ($versionFlags === []) { + $versionFlags = ['0']; + $flagsByPhpVersions[$versionId] = ['0']; + } + if ($previousVersionId !== null && $flagsByPhpVersions[$previousVersionId] === $versionFlags) { + unset($flagsByPhpVersions[$versionId]); + } else { + $previousVersionId = $versionId; + } + } + + $flagCount = count($flagsByPhpVersions); + + // Do not add a condition unnecessarily when the only version is the same as the minimally supported one + if ($flagCount === 1) { + reset($flagsByPhpVersions); + $firstVersion = key($flagsByPhpVersions); + if ($firstVersion === $phpVersionIdMinimumCompatibility) { + return sprintf($codeTemplate, implode("|", reset($flagsByPhpVersions))); + } + } + + // Add the necessary conditions around the code using the version-specific flags + $code = ''; + $i = 0; + foreach (array_reverse($flagsByPhpVersions, true) as $version => $versionFlags) { + $if = $i === 0 ? "#if" : "#elif"; + $endif = $i === $flagCount - 1 ? "#endif\n" : ""; + + $code .= "$if (PHP_VERSION_ID >= $version)\n"; + + $code .= sprintf($codeTemplate, implode("|", $versionFlags)); + $code .= $endif; + + $i++; + } + + return $code; + } +} + class FuncInfo { public /* readonly */ FunctionOrMethodName $name; private /* readonly */ int $classFlags; @@ -1317,10 +1374,10 @@ public function __construct( public function isMethod(): bool { - return $this->name->isMethod(); + return $this->name instanceof MethodName; } - public function isFinalMethod(): bool + private function isFinalMethod(): bool { return ($this->flags & Modifiers::FINAL) || ($this->classFlags & Modifiers::FINAL); } @@ -1331,7 +1388,7 @@ public function isInstanceMethod(): bool } /** @return string[] */ - public function getModifierNames(): array + private function getModifierNames(): array { if (!$this->isMethod()) { return []; @@ -1371,7 +1428,7 @@ private function hasParamWithUnknownDefaultValue(): bool return false; } - public function equalsApartFromNameAndRefcount(FuncInfo $other): bool { + private function equalsApartFromNameAndRefcount(FuncInfo $other): bool { if (count($this->args) !== count($other->args)) { return false; } @@ -1414,27 +1471,21 @@ public function getFramelessDeclaration(): ?string { return null; } - $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; - $code = ''; - - if (!$php84MinimumCompatibility) { - $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; - } - + $infos = ''; foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity});\n"; + $infos .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n"; } $code .= 'static const zend_frameless_function_info ' . $this->getFramelessFunctionInfosName() . "[] = {\n"; - foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { - $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n"; - } + $code .= $infos; $code .= "\t{ 0 },\n"; $code .= "};\n"; + $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; if (!$php84MinimumCompatibility) { - $code .= "#endif\n"; + return "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n$code#endif\n"; } return $code; @@ -1445,14 +1496,6 @@ private function getFramelessFunctionInfosName(): string { } public function getFunctionEntry(): string { - $code = ""; - - $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; - $isVanillaEntry = $this->alias === null && !$this->supportsCompileTimeEval && $this->exposedDocComment === null && empty($this->framelessFunctionInfos); - $argInfoName = $this->getArgInfoName(); - $flagsByPhpVersions = $this->getArginfoFlagsByPhpVersions(); - $functionEntryCode = null; - if (!empty($this->framelessFunctionInfos)) { if ($this->isMethod()) { throw new Exception('Frameless methods are not supported yet'); @@ -1465,6 +1508,10 @@ public function getFunctionEntry(): string { } } + $isVanillaEntry = $this->alias === null && !$this->supportsCompileTimeEval && $this->exposedDocComment === null && empty($this->framelessFunctionInfos); + $argInfoName = $this->getArgInfoName(); + $flagsByPhpVersions = $this->getArginfoFlagsByPhpVersions(); + if ($this->isMethod()) { $zendName = '"' . $this->name->methodName . '"'; if ($this->alias) { @@ -1475,80 +1522,60 @@ public function getFunctionEntry(): string { } else { throw new Error("Cannot happen"); } + } elseif ($this->flags & Modifiers::ABSTRACT) { + $name = "NULL"; } else { - if ($this->flags & Modifiers::ABSTRACT) { - $name = "NULL"; - } else { - $name = "zim_" . $this->name->getDeclarationClassName() . "_" . $this->name->methodName; - - if ($isVanillaEntry) { - $template = "\tZEND_ME(" . $this->name->getDeclarationClassName() . ", " . $this->name->methodName . ", $argInfoName, %s)\n"; - $flagsCode = generateVersionDependentFlagCode( - $template, - $flagsByPhpVersions, - $this->minimumPhpVersionIdCompatibility - ); - $functionEntryCode = rtrim($flagsCode); - } + $name = "zim_" . $this->name->getDeclarationClassName() . "_" . $this->name->methodName; + + if ($isVanillaEntry) { + $template = "\tZEND_ME(" . $this->name->getDeclarationClassName() . ", " . $this->name->methodName . ", $argInfoName, %s)\n"; + $flagsCode = $flagsByPhpVersions->generateVersionDependentFlagCode( + $template, + $this->minimumPhpVersionIdCompatibility + ); + return rtrim($flagsCode) . "\n"; } } } else if ($this->name instanceof FunctionName) { $functionName = $this->name->getFunctionName(); $declarationName = $this->alias ? $this->alias->getNonNamespacedName() : $this->name->getDeclarationName(); + $name = "zif_$declarationName"; if ($this->name->getNamespace()) { $namespace = addslashes($this->name->getNamespace()); $zendName = "ZEND_NS_NAME(\"$namespace\", \"$functionName\")"; - $name = "zif_$declarationName"; } else { - $zendName = '"' . $functionName . '"'; - $name = "zif_$declarationName"; - // Can only use ZEND_FE() if we have no flags for *all* versions - $hasFlags = false; - foreach ($flagsByPhpVersions as $flags) { - if ($flags !== ['0']) { - $hasFlags = true; - break; - } - } - if ($isVanillaEntry && !$hasFlags) { - $functionEntryCode = "\tZEND_FE($declarationName, $argInfoName)"; + if ($isVanillaEntry && $flagsByPhpVersions->isEmpty()) { + return "\tZEND_FE($declarationName, $argInfoName)\n"; } + $zendName = '"' . $functionName . '"'; } } else { throw new Error("Cannot happen"); } - if ($functionEntryCode !== null) { - $code .= "$functionEntryCode\n"; - } else { - if (!$php84MinimumCompatibility) { - $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; - } - - $php84AndAboveFlags = array_slice($flagsByPhpVersions, 5, null, true); - $docComment = $this->exposedDocComment ? '"' . $this->exposedDocComment->escape() . '"' : "NULL"; - $framelessFuncInfosName = !empty($this->framelessFunctionInfos) ? $this->getFramelessFunctionInfosName() : "NULL"; + $docComment = $this->exposedDocComment ? '"' . $this->exposedDocComment->escape() . '"' : "NULL"; + $framelessFuncInfosName = !empty($this->framelessFunctionInfos) ? $this->getFramelessFunctionInfosName() : "NULL"; - $code .= generateVersionDependentFlagCode( - "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s, $framelessFuncInfosName, $docComment)\n", - $php84AndAboveFlags, - PHP_84_VERSION_ID - ); + // Assume 8.4+ here, if older versions are supported this is conditional + $code = $flagsByPhpVersions->generateVersionDependentFlagCode( + "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s, $framelessFuncInfosName, $docComment)\n", + PHP_84_VERSION_ID + ); - if (!$php84MinimumCompatibility) { - $code .= "#else\n"; + $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; + if (!$php84MinimumCompatibility) { + $code = "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n$code"; + $code .= "#else\n"; - $flags = array_slice($flagsByPhpVersions, 0, 4, true); - $code .= generateVersionDependentFlagCode( - "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s)\n", - $flags, - $this->minimumPhpVersionIdCompatibility - ); + $code .= $flagsByPhpVersions->generateVersionDependentFlagCode( + "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s)\n", + $this->minimumPhpVersionIdCompatibility, + PHP_83_VERSION_ID + ); - $code .= "#endif\n"; - } + $code .= "#endif\n"; } return $code; @@ -1589,8 +1616,7 @@ public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibil $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; } - /** @return array */ - private function getArginfoFlagsByPhpVersions(): array + private function getArginfoFlagsByPhpVersions(): VersionFlags { $flags = []; @@ -1628,39 +1654,21 @@ private function getArginfoFlagsByPhpVersions(): array } } - $php82AndAboveFlags = $flags; + $flags = new VersionFlags($flags); + if ($this->isMethod() === false && $this->supportsCompileTimeEval) { - $php82AndAboveFlags[] = "ZEND_ACC_COMPILE_TIME_EVAL"; + $flags->addForVersionsAbove("ZEND_ACC_COMPILE_TIME_EVAL", PHP_82_VERSION_ID); } - $php85AndAboveFlags = $php82AndAboveFlags; foreach ($this->attributes as $attr) { switch ($attr->class) { case "NoDiscard": - $php85AndAboveFlags[] = "ZEND_ACC_NODISCARD"; + $flags->addForVersionsAbove("ZEND_ACC_NODISCARD", PHP_85_VERSION_ID); break; } } - if (empty($flags)) { - $flags[] = "0"; - } - if (empty($php82AndAboveFlags)) { - $php82AndAboveFlags[] = "0"; - } - if (empty($php85AndAboveFlags)) { - $php85AndAboveFlags[] = "0"; - } - - return [ - PHP_70_VERSION_ID => $flags, - PHP_80_VERSION_ID => $flags, - PHP_81_VERSION_ID => $flags, - PHP_82_VERSION_ID => $php82AndAboveFlags, - PHP_83_VERSION_ID => $php82AndAboveFlags, - PHP_84_VERSION_ID => $php82AndAboveFlags, - PHP_85_VERSION_ID => $php85AndAboveFlags, - ]; + return $flags; } private function generateRefSect1(DOMDocument $doc, string $role): DOMElement { @@ -2454,10 +2462,7 @@ abstract protected function getFieldSynopsisValueString(array $allConstInfos): ? abstract public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void; - /** - * @return array - */ - protected function getFlagsByPhpVersion(): array + protected function getFlagsByPhpVersion(): VersionFlags { $flags = "ZEND_ACC_PUBLIC"; if ($this->flags & Modifiers::PROTECTED) { @@ -2466,15 +2471,7 @@ protected function getFlagsByPhpVersion(): array $flags = "ZEND_ACC_PRIVATE"; } - return [ - PHP_70_VERSION_ID => [$flags], - PHP_80_VERSION_ID => [$flags], - PHP_81_VERSION_ID => [$flags], - PHP_82_VERSION_ID => [$flags], - PHP_83_VERSION_ID => [$flags], - PHP_84_VERSION_ID => [$flags], - PHP_85_VERSION_ID => [$flags], - ]; + return new VersionFlags([$flags]); } protected function getTypeCode(string $variableLikeName, string &$code): string @@ -2575,23 +2572,6 @@ protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fie } } - /** - * @param array $flags - * @return array - */ - protected function addFlagForVersionsAbove(array $flags, string $flag, int $minimumVersionId): array - { - $write = false; - - foreach ($flags as $version => $versionFlags) { - if ($version === $minimumVersionId || $write === true) { - $flags[$version][] = $flag; - $write = true; - } - } - - return $flags; - } } class ConstInfo extends VariableLike @@ -2688,41 +2668,35 @@ protected function getFieldSynopsisValueString(array $allConstInfos): ?string return $this->valueString; } - public function getPredefinedConstantTerm(DOMDocument $doc, int $indentationLevel): DOMElement { + private function getPredefinedConstantElement( + DOMDocument $doc, + int $indentationLevel, + string $name + ): DOMElement { $indentation = str_repeat(" ", $indentationLevel); - $termElement = $doc->createElement("term"); + $element = $doc->createElement($name); $constantElement = $doc->createElement("constant"); $constantElement->textContent = $this->name->__toString(); $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc); - $termElement->appendChild(new DOMText("\n$indentation ")); - $termElement->appendChild($constantElement); - $termElement->appendChild(new DOMText("\n$indentation (")); - $termElement->appendChild($typeElement); - $termElement->appendChild(new DOMText(")\n$indentation")); + $element->appendChild(new DOMText("\n$indentation ")); + $element->appendChild($constantElement); + $element->appendChild(new DOMText("\n$indentation (")); + $element->appendChild($typeElement); + $element->appendChild(new DOMText(")\n$indentation")); - return $termElement; + return $element; } - public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLevel): DOMElement { - $indentation = str_repeat(" ", $indentationLevel); - - $entryElement = $doc->createElement("entry"); - - $constantElement = $doc->createElement("constant"); - $constantElement->textContent = $this->name->__toString(); - $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc); - - $entryElement->appendChild(new DOMText("\n$indentation ")); - $entryElement->appendChild($constantElement); - $entryElement->appendChild(new DOMText("\n$indentation (")); - $entryElement->appendChild($typeElement); - $entryElement->appendChild(new DOMText(")\n$indentation")); + public function getPredefinedConstantTerm(DOMDocument $doc, int $indentationLevel): DOMElement { + return $this->getPredefinedConstantElement($doc, $indentationLevel, "term"); + } - return $entryElement; + public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLevel): DOMElement { + return $this->getPredefinedConstantElement($doc, $indentationLevel, "entry"); } public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { @@ -2754,7 +2728,7 @@ public function getDeclaration(array $allConstInfos): string // Condition will be added by generateCodeWithConditions() - if ($this->name->isClassConst()) { + if ($this->name instanceof ClassConstName) { $code = $this->getClassConstDeclaration($value, $allConstInfos); } else { $code = $this->getGlobalConstDeclaration($value); @@ -2840,9 +2814,8 @@ private function getClassConstDeclaration(EvaluatedValue $value, array $allConst } $template .= "zend_declare_typed_class_constant(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode, $typeCode);\n"; - $code .= generateVersionDependentFlagCode( + $code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode( $template, - $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility ); } @@ -2858,9 +2831,8 @@ private function getClassConstDeclaration(EvaluatedValue $value, array $allConst $template = "\t"; } $template .= "zend_declare_class_constant_ex(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode);\n"; - $code .= generateVersionDependentFlagCode( + $code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode( $template, - $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility ); } @@ -2910,20 +2882,17 @@ private function getValueAssertion(EvaluatedValue $value): string throw new Exception("Unimplemented constant type"); } - /** - * @return array - */ - protected function getFlagsByPhpVersion(): array + protected function getFlagsByPhpVersion(): VersionFlags { $flags = parent::getFlagsByPhpVersion(); // $this->isDeprecated also accounts for any #[\Deprecated] attributes if ($this->isDeprecated) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID); } if ($this->flags & Modifiers::FINAL) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_FINAL", PHP_81_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_FINAL", PHP_81_VERSION_ID); } return $flags; @@ -3159,9 +3128,8 @@ public function getDeclaration(array $allConstInfos): string { $template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n"; } - $code .= generateVersionDependentFlagCode( + $code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode( $template, - $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility ); @@ -3224,29 +3192,26 @@ private function getString(string $propName): array { return $result; } - /** - * @return array - */ - protected function getFlagsByPhpVersion(): array + protected function getFlagsByPhpVersion(): VersionFlags { $flags = parent::getFlagsByPhpVersion(); if ($this->flags & Modifiers::STATIC) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_STATIC", PHP_70_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_STATIC", PHP_70_VERSION_ID); } if ($this->flags & Modifiers::FINAL) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_FINAL", PHP_84_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_FINAL", PHP_84_VERSION_ID); } if ($this->flags & Modifiers::READONLY) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_81_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_READONLY", PHP_81_VERSION_ID); } elseif ($this->classFlags & Modifiers::READONLY) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_82_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_READONLY", PHP_82_VERSION_ID); } if ($this->isVirtual) { - $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_VIRTUAL", PHP_84_VERSION_ID); + $flags->addForVersionsAbove("ZEND_ACC_VIRTUAL", PHP_84_VERSION_ID); } return $flags; @@ -3480,13 +3445,17 @@ public function getRegistration(array $allConstInfos): string $code .= "{\n"; + $flags = $this->getFlagsByPhpVersion(); + $classMethods = ($this->funcInfos === []) ? 'NULL' : "class_{$escapedName}_methods"; if ($this->type === "enum") { $name = addslashes((string) $this->name); $backingType = $this->enumBackingType ? $this->enumBackingType->toTypeCode() : "IS_UNDEF"; $code .= "\tzend_class_entry *class_entry = zend_register_internal_enum(\"$name\", $backingType, $classMethods);\n"; - $code .= generateVersionDependentFlagCode("\tclass_entry->ce_flags = %s;\n", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility); + if (!$flags->isEmpty()) { + $code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode("\tclass_entry->ce_flags = %s;\n", $this->phpVersionIdMinimumCompatibility); + } } else { $code .= "\tzend_class_entry ce, *class_entry;\n\n"; if (count($this->name->getParts()) > 1) { @@ -3504,7 +3473,7 @@ public function getRegistration(array $allConstInfos): string } $template = "\tclass_entry = zend_register_internal_class_with_flags(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ", %s);\n"; - $entries = generateVersionDependentFlagCode($template, $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility ? max($this->phpVersionIdMinimumCompatibility, PHP_84_VERSION_ID) : null); + $entries = $flags->generateVersionDependentFlagCode($template, $this->phpVersionIdMinimumCompatibility ? max($this->phpVersionIdMinimumCompatibility, PHP_84_VERSION_ID) : null); if ($entries !== '') { $code .= $entries; } else { @@ -3515,12 +3484,16 @@ public function getRegistration(array $allConstInfos): string $code .= "#else\n"; $code .= "\tclass_entry = zend_register_internal_class_ex(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ");\n"; - $code .= generateVersionDependentFlagCode("\tclass_entry->ce_flags |= %s;\n", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility); + if (!$flags->isEmpty()) { + $code .= $flags->generateVersionDependentFlagCode("\tclass_entry->ce_flags |= %s;\n", $this->phpVersionIdMinimumCompatibility); + } $code .= "#endif\n"; } } else { $code .= "\tclass_entry = zend_register_internal_interface(&ce);\n"; - $code .= generateVersionDependentFlagCode("\tclass_entry->ce_flags |= %s;\n", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility); + if (!$flags->isEmpty()) { + $code .= $flags->generateVersionDependentFlagCode("\tclass_entry->ce_flags |= %s;\n", $this->phpVersionIdMinimumCompatibility); + } } } @@ -3565,11 +3538,17 @@ function (Name $item) { foreach ($this->propertyInfos as $property) { $code .= $property->getDeclaration($allConstInfos); } + // Reusable strings for wrapping conditional PHP 8.0+ code + if ($php80MinimumCompatibility) { + $php80CondStart = ''; + $php80CondEnd = ''; + } else { + $php80CondStart = "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; + $php80CondEnd = "#endif\n"; + } if (!empty($this->attributes)) { - if (!$php80MinimumCompatibility) { - $code .= "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; - } + $code .= $php80CondStart; foreach ($this->attributes as $key => $attribute) { $code .= $attribute->generateCode( @@ -3580,45 +3559,25 @@ function (Name $item) { ); } - if (!$php80MinimumCompatibility) { - $code .= "#endif\n"; - } + $code .= $php80CondEnd; } if ($attributeInitializationCode = generateConstantAttributeInitialization($this->constInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) { - if (!$php80MinimumCompatibility) { - $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; - } - + $code .= $php80CondStart; $code .= "\n" . $attributeInitializationCode; - - if (!$php80MinimumCompatibility) { - $code .= "#endif\n"; - } + $code .= $php80CondEnd; } if ($attributeInitializationCode = generatePropertyAttributeInitialization($this->propertyInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility)) { - if (!$php80MinimumCompatibility) { - $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; - } - + $code .= $php80CondStart; $code .= "\n" . $attributeInitializationCode; - - if (!$php80MinimumCompatibility) { - $code .= "#endif\n"; - } + $code .= $php80CondEnd; } if ($attributeInitializationCode = generateFunctionAttributeInitialization($this->funcInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) { - if (!$php80MinimumCompatibility) { - $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")\n"; - } - + $code .= $php80CondStart; $code .= "\n" . $attributeInitializationCode; - - if (!$php80MinimumCompatibility) { - $code .= "#endif\n"; - } + $code .= $php80CondEnd; } $code .= "\n\treturn class_entry;\n"; @@ -3636,10 +3595,7 @@ function (Name $item) { return $code; } - /** - * @return array - */ - private function getFlagsByPhpVersion(): array + private function getFlagsByPhpVersion(): VersionFlags { $php70Flags = []; @@ -3659,44 +3615,28 @@ private function getFlagsByPhpVersion(): array $php70Flags[] = "ZEND_ACC_DEPRECATED"; } - $php80Flags = $php70Flags; + $flags = new VersionFlags($php70Flags); if ($this->isStrictProperties) { - $php80Flags[] = "ZEND_ACC_NO_DYNAMIC_PROPERTIES"; + $flags->addForVersionsAbove("ZEND_ACC_NO_DYNAMIC_PROPERTIES", PHP_80_VERSION_ID); } - $php81Flags = $php80Flags; - if ($this->isNotSerializable) { - $php81Flags[] = "ZEND_ACC_NOT_SERIALIZABLE"; + $flags->addForVersionsAbove("ZEND_ACC_NOT_SERIALIZABLE", PHP_81_VERSION_ID); } - $php82Flags = $php81Flags; - if ($this->flags & Modifiers::READONLY) { - $php82Flags[] = "ZEND_ACC_READONLY_CLASS"; + $flags->addForVersionsAbove("ZEND_ACC_READONLY_CLASS", PHP_82_VERSION_ID); } foreach ($this->attributes as $attr) { if ($attr->class === "AllowDynamicProperties") { - $php82Flags[] = "ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES"; + $flags->addForVersionsAbove("ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES", PHP_82_VERSION_ID); break; } } - $php83Flags = $php82Flags; - $php84Flags = $php83Flags; - $php85Flags = $php84Flags; - - return [ - PHP_70_VERSION_ID => $php70Flags, - PHP_80_VERSION_ID => $php80Flags, - PHP_81_VERSION_ID => $php81Flags, - PHP_82_VERSION_ID => $php82Flags, - PHP_83_VERSION_ID => $php83Flags, - PHP_84_VERSION_ID => $php84Flags, - PHP_85_VERSION_ID => $php85Flags, - ]; + return $flags; } public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { @@ -4308,13 +4248,25 @@ protected function pName_FullyQualified(Name\FullyQualified $node): string { $stmts = $parser->parse($code); $nodeTraverser->traverse($stmts); - $fileTags = DocCommentTag::parseDocComments(getFileDocComments($stmts)); + $fileTags = DocCommentTag::parseDocComments(self::getFileDocComments($stmts)); $fileInfo = new FileInfo($fileTags); $fileInfo->handleStatements($stmts, $prettyPrinter); return $fileInfo; } + /** @return DocComment[] */ + private static function getFileDocComments(array $stmts): array { + if (empty($stmts)) { + return []; + } + + return array_filter( + $stmts[0]->getComments(), + static fn ( $comment ): bool => $comment instanceof DocComment + ); + } + private function handleStatements(array $stmts, PrettyPrinterAbstract $prettyPrinter): void { $conds = []; foreach ($stmts as $stmt) { @@ -4486,6 +4438,17 @@ private static function handlePreprocessorConditions(array &$conds, Stmt $stmt): return empty($conds) ? null : implode(' && ', $conds); } + + /** @param array $allConstInfos */ + public function generateClassEntryCode(array $allConstInfos): string { + $code = ""; + + foreach ($this->classInfos as $class) { + $code .= "\n" . $class->getRegistration($allConstInfos); + } + + return $code; + } } class DocCommentTag { @@ -5039,18 +5002,6 @@ function parseClass( ); } -/** @return DocComment[] */ -function getFileDocComments(array $stmts): array { - if (empty($stmts)) { - return []; - } - - return array_filter( - $stmts[0]->getComments(), - static fn ( $comment ): bool => $comment instanceof DocComment - ); -} - /** * @template T * @param iterable $infos @@ -5204,18 +5155,7 @@ static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarat $code .= "}\n"; } - $code .= generateClassEntryCode($fileInfo, $allConstInfos); - } - - return $code; -} - -/** @param array $allConstInfos */ -function generateClassEntryCode(FileInfo $fileInfo, array $allConstInfos): string { - $code = ""; - - foreach ($fileInfo->classInfos as $class) { - $code .= "\n" . $class->getRegistration($allConstInfos); + $code .= $fileInfo->generateClassEntryCode($allConstInfos); } return $code; @@ -5228,19 +5168,13 @@ function generateFunctionEntries(?Name $className, array $funcInfos, ?string $co return ''; } - $code = "\n"; - - if ($cond) { - $code .= "#if {$cond}\n"; - } - $functionEntryName = "ext_functions"; if ($className) { $underscoreName = implode("_", $className->getParts()); $functionEntryName = "class_{$underscoreName}_methods"; } - $code .= "static const zend_function_entry {$functionEntryName}[] = {\n"; + $code = "\nstatic const zend_function_entry {$functionEntryName}[] = {\n"; $code .= generateCodeWithConditions($funcInfos, "", static function (FuncInfo $funcInfo) { return $funcInfo->getFunctionEntry(); }, $cond); @@ -5248,7 +5182,7 @@ function generateFunctionEntries(?Name $className, array $funcInfos, ?string $co $code .= "};\n"; if ($cond) { - $code .= "#endif\n"; + $code = "\n#if {$cond}{$code}#endif\n"; } return $code; @@ -5411,80 +5345,6 @@ function generateOptimizerInfo(array $funcMap): string { return $code; } -/** - * @param array $flagsByPhpVersions - * @return string - */ -function generateVersionDependentFlagCode(string $codeTemplate, array $flagsByPhpVersions, ?int $phpVersionIdMinimumCompatibility): string -{ - $phpVersions = ALL_PHP_VERSION_IDS; - sort($phpVersions); - $currentPhpVersion = end($phpVersions); - - // No version compatibility is needed - if ($phpVersionIdMinimumCompatibility === null) { - if (empty($flagsByPhpVersions[$currentPhpVersion])) { - return ''; - } - - return sprintf($codeTemplate, implode("|", $flagsByPhpVersions[$currentPhpVersion])); - } - - // Remove flags which depend on a PHP version below the minimally supported one - ksort($flagsByPhpVersions); - $index = array_search($phpVersionIdMinimumCompatibility, array_keys($flagsByPhpVersions)); - if ($index === false) { - throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMinimumCompatibility\""); - } - $flagsByPhpVersions = array_slice($flagsByPhpVersions, $index, null, true); - - // Remove empty version-specific flags - $flagsByPhpVersions = array_filter($flagsByPhpVersions); - - // There are no version-specific flags - if (empty($flagsByPhpVersions)) { - return ''; - } - - // Remove version-specific flags which don't differ from the previous one - $previousVersionId = null; - foreach ($flagsByPhpVersions as $versionId => $versionFlags) { - if ($previousVersionId !== null && $flagsByPhpVersions[$previousVersionId] === $versionFlags) { - unset($flagsByPhpVersions[$versionId]); - } else { - $previousVersionId = $versionId; - } - } - - $flagCount = count($flagsByPhpVersions); - - // Do not add a condition unnecessarily when the only version is the same as the minimally supported one - if ($flagCount === 1) { - reset($flagsByPhpVersions); - $firstVersion = key($flagsByPhpVersions); - if ($firstVersion === $phpVersionIdMinimumCompatibility) { - return sprintf($codeTemplate, implode("|", reset($flagsByPhpVersions))); - } - } - - // Add the necessary conditions around the code using the version-specific flags - $code = ''; - $i = 0; - foreach (array_reverse($flagsByPhpVersions, true) as $version => $versionFlags) { - $if = $i === 0 ? "#if" : "#elif"; - $endif = $i === $flagCount - 1 ? "#endif\n" : ""; - - $code .= "$if (PHP_VERSION_ID >= $version)\n"; - - $code .= sprintf($codeTemplate, implode("|", $versionFlags)); - $code .= $endif; - - $i++; - } - - return $code; -} - /** * @param array $constMap * @param array $undocumentedConstMap @@ -6376,7 +6236,7 @@ function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc if ($verifyManual) { foreach ($undocumentedConstMap as $constName => $info) { - if ($info->name->isClassConst() || $info->isUndocumentable) { + if ($info->name instanceof ClassConstName || $info->isUndocumentable) { continue; }