diff --git a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md index e58263cd..305a5279 100644 --- a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md +++ b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md @@ -25,7 +25,7 @@ | 静态可解析的 `new NativeClass()` 使用 Native Heap | `CompilerBase::parseNew()`、`php::nativeConstruct()` | `basic.phpt`、`construction-gc-roots.phpt` | 已验证 | | `new (表达式)()` 保持普通 PHP 动态实例化 | `parseNew()` 只对 `Node\\Name` 进入 Native 分支 | `testLeavesDynamicClassExpressionsToTheOrdinaryPhpPath` | 已验证 | | Native 对象本身不能充当动态 class target | `assertNotNativeObjectDynamicClassTarget()` | dynamic new/static call/class constant 负向测试 | 已验证 | -| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 128 项 `NativeClassValidationTest` | 已验证 | +| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 131 项 `NativeClassValidationTest` | 已验证 | ## 3. 属性与固定布局 @@ -113,7 +113,8 @@ |---|---|---|---| | Native class 前向声明不依赖文件顺序 | declaration discovery pre-pass | `testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing` | 已验证 | | global Native slot ABI 在任一 C++ 文件生成前确定 | `NativeGlobalDiscovery`、`NativeGlobalTypeResolver` | `testDiscoversNativeGlobalSlotBeforeEarlierReaderIsConverted`,实际双文件构建 | 已验证 | -| `global $slot` 与字面量 `$GLOBALS['slot']` 使用同一 Native root slot | literal global slot lowering、request root registration | `global-and-static.phpt`、跨文件 `$GLOBALS` fixture | 已验证 | +| `global $slot` 与静态可解析的 `$GLOBALS[...]` 使用同一 Native root slot | literal/constant global slot lowering、request root registration | `global-and-static.phpt`、跨文件 Closure/常量 `$GLOBALS` fixture | 已验证 | +| 动态 `$GLOBALS[$key]` 不得承载 Native Object | dynamic Zend boundary validation | `testRejectsNativeObjectStoredThroughDynamicGlobalsKey` | 已验证 | | global slot 固定首个 Native 类型,只允许子类或 null | global registration/type validation | `global-and-static.phpt`、global type change 负向测试 | 已验证 | | 未使用 Native Class 的项目跳过 Native global pre-pass | `discoverNativeGlobalObjects()` fast return | 源码检查、全量 PHPUnit | 已验证 | @@ -126,6 +127,6 @@ vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php --gtest_filter='wren_gc.*:native_gc.*' ``` -本次结果分别为:69/69 PHPT、128/128 PHPUnit、17/17 PHPX C++ tests。 -最终合入前仍需执行编译器完整 PHPUnit、完整 PHPT 和 PHPX 完整测试,防止 Native -分支的公共 hook 影响普通对象模型。 +本次结果分别为:69/69 PHPT、131/131 PHPUnit、17/17 PHPX C++ tests。 +完整回归结果为:编译器 PHPUnit 1431/1431、编译器 PHPT 1037/1037(另有 2 项按 +环境跳过)、PHPX C++ tests 1016/1016。Native 分支的公共 hook 未影响普通对象模型。 diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index b5f499af..dbb1791f 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -507,7 +507,9 @@ struct FunctionNativeRoots { - 只登记可能跨越 Native allocation 或显式 GC safe point 存活的变量。 - 普通方法 receiver 由调用者的 root 或对象图保持存活,不重复登记。 - 临时对象如果跨越一次可能触发 GC 的调用,必须先写入 root slot。 -- Native Class 允许保存在 TypePHP global 和 static local 中。这些槽不进入 Zend `$GLOBALS`,而是生成独立 Native 指针槽。 +- Native Class 允许保存在 TypePHP global 和 static local 中。这些槽不注册到 Zend symbol table,而是生成独立 Native 指针槽。 +- `global $slot` 与 `$GLOBALS['slot']` 使用同一个 Native 指针槽;`$GLOBALS` 的键也可以是编译期可求值为字符串的全局常量、类常量或常量表达式。 +- `$GLOBALS[$dynamicKey]` 仍按 PHP 语法走 Zend HashTable。由于 Native Object 没有 zval 表示,动态键不能用于读写 Native global。 - ZTS 构建中的 global/static 指针槽和 static 初始化状态均使用 `THREAD_LOCAL`,不同线程之间不共享 Native 对象。 - RINIT 将这些槽登记为 request root;RSHUTDOWN 清空槽和初始化状态,随后由 Native Heap 统一 finalization 和回收。 @@ -1289,6 +1291,7 @@ $json = json_encode($nativeObject->toArray()); | Native 元素 Std Container 转 PHP array/mixed 或作为 PHP 参数 | 不支持;裸指针不得越过 ZendVM value boundary | | Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC | | TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 | +| `$GLOBALS` 访问 Native global | 字面量或编译期可求值的字符串常量映射到同一 C++ slot;动态键不支持 Native Object | | global/static local 类型 | 第一次 Native 赋值固定 C++ slot 类型;后续可写入其 Native 子类或 null,不可改为基类/无关类 | | Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 | | late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 | diff --git a/phpunit/code/native-class-dynamic-globals-write.php b/phpunit/code/native-class-dynamic-globals-write.php new file mode 100644 index 00000000..d6fa6ea5 --- /dev/null +++ b/phpunit/code/native-class-dynamic-globals-write.php @@ -0,0 +1,12 @@ +value; } + +function readNativeForwardClosureGlobal(): int +{ + global $nativeForwardClosureGlobal; + return $nativeForwardClosureGlobal->value; +} diff --git a/phpunit/code/native-class-global-forward/b.php b/phpunit/code/native-class-global-forward/b.php index 1fc318b4..1ea0a032 100644 --- a/phpunit/code/native-class-global-forward/b.php +++ b/phpunit/code/native-class-global-forward/b.php @@ -71,3 +71,13 @@ function initializeNativeForwardGlobalsArray(): void { $GLOBALS['nativeForwardGlobalsArray'] = new NativeForwardGlobalValue(); } + +const NATIVE_FORWARD_CLOSURE_GLOBAL = 'nativeForwardClosureGlobal'; + +function initializeNativeForwardClosureGlobal(): void +{ + $initialize = static function (): void { + $GLOBALS[NATIVE_FORWARD_CLOSURE_GLOBAL] = new NativeForwardGlobalValue(); + }; + $initialize(); +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index 7b91ee14..e38ef6d6 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -63,6 +63,10 @@ final class NativeClassValidationTest extends \BaseTest '/php::nativeDeref\\(tmp_var_\\d+, "NativeForwardGlobalValue"\\)\\.value/', $code, ); + self::assertStringContainsString( + 'php::nativeDeref(nativeForwardClosureGlobal, "NativeForwardGlobalValue").value', + $code, + ); self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code); } @@ -308,6 +312,15 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-php-array.php'); } + public function testRejectsNativeObjectStoredThroughDynamicGlobalsKey(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage( + 'Native objects cannot be stored in PHP arrays, PHP object properties, static properties, or mixed variables', + ); + $this->compile('native-class-dynamic-globals-write.php'); + } + public function testRejectsNativeObjectStoredInPhpObjectProperty(): void { $this->expectException(TestError::class); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index a64eddbd..02705dd8 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -83,6 +83,7 @@ use TypePhp\Symbol\SymbolRepository; use TypePhp\TypeSystem\CompositeTypeCheckerTrait; use TypePhp\TypeSystem\NativeTypeCompatibilityTrait; use TypePhp\NativeClass\NativeClassSupportTrait; +use TypePhp\NativeClass\NativeGlobalTypeResolver; use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\ArrayItem; @@ -437,6 +438,8 @@ class CompilerBase implements PropertyAccessContext protected array $globalVars = []; /** @var array Global/static Native pointer slot => class name. */ protected array $nativeGlobalObjects = []; + /** Immutable metadata shared by Native global pre-discovery and lowering. */ + protected ?NativeGlobalTypeResolver $nativeGlobalTypeResolver = null; /** @var array Lowercase class name => declared Native class name. */ protected array $nativeClassDeclarations = []; /** @var array Request-reset initialization flags for Native static locals. */ @@ -1974,7 +1977,7 @@ class CompilerBase implements PropertyAccessContext return $this->getObjectType($object); } } - $globalSlot = $this->getLiteralGlobalsSlot($expr); + $globalSlot = $this->getStaticGlobalsSlot($expr); if ($globalSlot !== null && isset($this->nativeGlobalObjects[$globalSlot])) { return $this->nativeGlobalObjects[$globalSlot]; } diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 8778efcf..bff3d2bb 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -375,6 +375,11 @@ trait NativeClassSupportTrait } } + /** + * Resolve only the `$GLOBALS['literal']` form. A fixed key can share the + * compiler-owned Native global slot without touching Zend's symbol table; + * dynamically addressed `$GLOBALS[$name]` remains a Zend value boundary. + */ protected function getLiteralGlobalsSlot(NodeAbstract $expression): ?string { if (!$expression instanceof Node\Expr\ArrayDimFetch @@ -387,6 +392,66 @@ trait NativeClassSupportTrait return $expression->dim->value; } + /** + * Resolve a statically known `$GLOBALS[...]` key to the compiler-owned + * Native global slot. Dynamic keys deliberately return null and remain on + * the ordinary Zend HashTable path. + */ + protected function getStaticGlobalsSlot(NodeAbstract $expression): ?string + { + $literal = $this->getLiteralGlobalsSlot($expression); + if ($literal !== null) { + return $literal; + } + if (!$expression instanceof Node\Expr\ArrayDimFetch + || !$expression->var instanceof Node\Expr\Variable + || $expression->var->name !== 'GLOBALS' + || $expression->dim === null + ) { + return null; + } + + // Preserve PHP visibility diagnostics even though the key itself is + // folded and never emitted as a runtime class-constant fetch. + $finder = new \PhpParser\NodeFinder(); + foreach ($finder->findInstanceOf($expression->dim, Node\Expr\ClassConstFetch::class) as $fetch) { + if (!$fetch->class instanceof Node\Name + || !$fetch->name instanceof Node\Identifier + || strcasecmp($fetch->name->toString(), 'class') === 0 + ) { + continue; + } + $class = $fetch->class->toString(); + if (strcasecmp($class, 'self') === 0 || strcasecmp($class, 'static') === 0) { + $class = $this->getFullClassName(); + } elseif (strcasecmp($class, 'parent') === 0) { + $class = $this->classDef?->extends ?? ''; + } else { + $resolved = $fetch->class->getAttribute('resolvedName'); + $class = $resolved instanceof Node\Name + ? $resolved->toString() + : $this->getNamespacedClassName($class); + } + if ($class !== '') { + $this->getClassConstValue( + $fetch, + $class, + $fetch->name->toString(), + $this->getFullClassName(), + ); + } + } + + $this->nativeGlobalTypeResolver ??= new NativeGlobalTypeResolver( + $this->symbols->classes(), + $this->constants, + ); + return $this->nativeGlobalTypeResolver->staticString( + $expression->dim, + $this->getFullClassName(), + ); + } + protected function getNativeObjectCppName(string|ClassDef $class): string { if ($class instanceof ClassDef) { diff --git a/src/NativeClass/NativeGlobalDiscovery.php b/src/NativeClass/NativeGlobalDiscovery.php index 9fe1a05e..6220060d 100644 --- a/src/NativeClass/NativeGlobalDiscovery.php +++ b/src/NativeClass/NativeGlobalDiscovery.php @@ -90,9 +90,6 @@ final class NativeGlobalDiscovery $globals = []; $this->collectGlobals($function->stmts, $globals); - if ($globals === [] && !$this->containsGlobalsArray($function->stmts)) { - return; - } $locals = []; $previousScope = $this->scopeClass; @@ -115,6 +112,51 @@ final class NativeGlobalDiscovery $this->scopeClass = $previousScope; } + /** + * Analyze a Closure as an independent PHP variable scope while retaining + * its lexical class scope. Native objects cannot be captured by a Zend + * Closure, but an ordinary captured object may still construct a Native + * object and a Closure may write a newly constructed object to a global. + * + * @param array $outerLocals + * @param list $result + */ + private function discoverClosure( + Node\Expr\Closure $closure, + array $outerLocals, + array &$result, + ): void { + if ($closure->stmts === null) { + return; + } + + $globals = []; + $this->collectGlobals($closure->stmts, $globals); + $locals = []; + if (!$closure->static && isset($outerLocals['this'])) { + $locals['this'] = $outerLocals['this']; + } + foreach ($closure->uses as $use) { + if ($use->var instanceof Node\Expr\Variable + && is_string($use->var->name) + && isset($outerLocals[$use->var->name]) + ) { + $locals[$use->var->name] = $outerLocals[$use->var->name]; + } + } + foreach ($closure->params as $parameter) { + if (!$parameter->var instanceof Node\Expr\Variable || !is_string($parameter->var->name)) { + continue; + } + $parameterClass = $this->classFromType($parameter->type); + if ($parameterClass !== null) { + $locals[$parameter->var->name] = $parameterClass; + } + } + + $this->analyzeNodes($closure->stmts, $globals, $locals, $result); + } + /** @param array $globals */ private function collectGlobals(mixed $node, array &$globals): void { @@ -143,33 +185,6 @@ final class NativeGlobalDiscovery } } - private function containsGlobalsArray(mixed $node): bool - { - if ($node === null) { - return false; - } - if (is_array($node)) { - foreach ($node as $item) { - if ($this->containsGlobalsArray($item)) { - return true; - } - } - return false; - } - if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { - return false; - } - if ($this->globalArrayName($node) !== null) { - return true; - } - foreach ($node->getSubNodeNames() as $name) { - if ($this->containsGlobalsArray($node->{$name})) { - return true; - } - } - return false; - } - /** * @param array $globals * @param array $locals @@ -190,7 +205,27 @@ final class NativeGlobalDiscovery } return; } - if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { + if (!$node instanceof NodeAbstract) { + return; + } + if ($node instanceof Node\Expr\Closure) { + $this->discoverClosure($node, $locals, $result); + return; + } + if ($node instanceof Node\Stmt\Function_) { + $this->discoverFunction($node, '', $result); + return; + } + if ($node instanceof Node\Stmt\ClassLike) { + $className = isset($node->namespacedName) + ? $node->namespacedName->toString() + : ($node->name?->toString() ?? ''); + foreach ($node->getMethods() as $method) { + $this->discoverFunction($method, $className, $result); + } + return; + } + if ($node instanceof Node\FunctionLike) { return; } if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp\Coalesce) { @@ -326,11 +361,11 @@ final class NativeGlobalDiscovery if (!$node instanceof Node\Expr\ArrayDimFetch || !$node->var instanceof Node\Expr\Variable || $node->var->name !== 'GLOBALS' - || !$node->dim instanceof Node\Scalar\String_ + || $node->dim === null ) { return null; } - return $node->dim->value; + return $this->resolver->staticString($node->dim, $this->scopeClass); } private function isNull(NodeAbstract $expression): bool diff --git a/src/NativeClass/NativeGlobalTypeResolver.php b/src/NativeClass/NativeGlobalTypeResolver.php index 5cde7961..293114d5 100644 --- a/src/NativeClass/NativeGlobalTypeResolver.php +++ b/src/NativeClass/NativeGlobalTypeResolver.php @@ -8,6 +8,9 @@ namespace TypePhp\NativeClass; +use PhpParser\ConstExprEvaluator; +use PhpParser\Node; +use PhpParser\NodeAbstract; use TypePhp\Entity\ClassDef; /** @@ -22,6 +25,12 @@ final class NativeGlobalTypeResolver /** @var array */ private array $classes = []; + /** @var array */ + private array $classDefinitions = []; + + /** @var array */ + private array $globalConstantExpressions = []; + /** @var array */ private array $nativeClasses = []; @@ -34,18 +43,30 @@ final class NativeGlobalTypeResolver /** @var array> */ private array $propertyClasses = []; - /** @param array $classes */ - public function __construct(array $classes) + /** + * @param array $classes + * @param array $constants + */ + public function __construct(array $classes, array $constants = []) { foreach ($classes as $class) { $name = $class->getNamespacedName(false); $key = strtolower(ltrim($name, '\\')); $this->classes[$key] = $name; + $this->classDefinitions[$key] = $class; if ($class->nativeObject) { $this->nativeClasses[$key] = true; } } + foreach ($constants as $constant) { + if (isset($constant->name) && is_string($constant->name) + && isset($constant->valueExpr) && $constant->valueExpr instanceof Node\Expr + ) { + $this->globalConstantExpressions[ltrim($constant->name, '\\')] = $constant->valueExpr; + } + } + foreach ($classes as $class) { $name = $class->getNamespacedName(false); $key = strtolower(ltrim($name, '\\')); @@ -144,4 +165,163 @@ final class NativeGlobalTypeResolver { return $this->parents[strtolower(ltrim($class, '\\'))] ?? null; } + + /** + * Evaluate a PHP constant expression used as a `$GLOBALS[...]` key. + * Returning null keeps the expression on the ordinary dynamic Zend path. + */ + public function staticString(NodeAbstract $expression, string $scopeClass = ''): ?string + { + if ($expression instanceof Node\Scalar\String_) { + return $expression->value; + } + + $visiting = []; + try { + $value = $this->evaluateConstantExpression($expression, $scopeClass, $visiting, 0); + } catch (\Throwable) { + return null; + } + return is_string($value) ? $value : null; + } + + /** @param array $visiting */ + private function evaluateConstantExpression( + NodeAbstract $expression, + string $scopeClass, + array &$visiting, + int $depth, + ): mixed { + if ($depth > 32 || !$expression instanceof Node\Expr) { + throw new \RuntimeException('Constant expression nesting is too deep'); + } + + $evaluator = new ConstExprEvaluator(function (Node\Expr $node) use ( + $scopeClass, + &$visiting, + $depth, + ): mixed { + if ($node instanceof Node\Expr\ConstFetch) { + foreach ($this->resolvedNameCandidates($node->name) as $name) { + $lower = strtolower($name); + if ($lower === 'true') { + return true; + } + if ($lower === 'false') { + return false; + } + if ($lower === 'null') { + return null; + } + if (isset($this->globalConstantExpressions[$name])) { + $key = 'global:' . $name; + if (isset($visiting[$key])) { + throw new \RuntimeException('Circular constant expression'); + } + $visiting[$key] = true; + try { + return $this->evaluateConstantExpression( + $this->globalConstantExpressions[$name], + $scopeClass, + $visiting, + $depth + 1, + ); + } finally { + unset($visiting[$key]); + } + } + if (defined($name)) { + return constant($name); + } + } + throw new \RuntimeException('Unresolved global constant'); + } + + if ($node instanceof Node\Expr\ClassConstFetch + && $node->class instanceof Node\Name + && $node->name instanceof Node\Identifier + ) { + $class = $this->resolvedClassName($node->class, $scopeClass); + $constant = $node->name->toString(); + if (strcasecmp($constant, 'class') === 0) { + return $class; + } + $classKey = strtolower(ltrim($class, '\\')); + while (isset($this->classDefinitions[$classKey])) { + $definition = $this->classDefinitions[$classKey]; + if ($definition->hasConstant($constant)) { + $constantDefinition = $definition->getConstant($constant); + if (!$constantDefinition->valueExpr instanceof Node\Expr) { + throw new \RuntimeException('Class constant has no static expression'); + } + $key = 'class:' . $classKey . '::' . $constant; + if (isset($visiting[$key])) { + throw new \RuntimeException('Circular class constant expression'); + } + $visiting[$key] = true; + try { + return $this->evaluateConstantExpression( + $constantDefinition->valueExpr, + $definition->getNamespacedName(false), + $visiting, + $depth + 1, + ); + } finally { + unset($visiting[$key]); + } + } + $parent = $this->parents[$classKey] ?? null; + if ($parent === null) { + break; + } + $classKey = strtolower($parent); + } + $runtimeConstant = $class . '::' . $constant; + if (defined($runtimeConstant)) { + return constant($runtimeConstant); + } + throw new \RuntimeException('Unresolved class constant'); + } + + throw new \RuntimeException('Unsupported constant expression'); + }); + + return $evaluator->evaluateDirectly($expression); + } + + private function resolvedClassName(Node\Name $name, string $scopeClass): string + { + $keyword = strtolower($name->toString()); + if ($keyword === 'self' || $keyword === 'static') { + if ($scopeClass === '') { + throw new \RuntimeException('Class-relative constant outside class scope'); + } + return ltrim($scopeClass, '\\'); + } + if ($keyword === 'parent') { + $parent = $this->parentClass($scopeClass); + if ($parent === null) { + throw new \RuntimeException('Parent-relative constant without a parent'); + } + return $parent; + } + $resolved = $name->getAttribute('resolvedName'); + return ltrim($resolved instanceof Node\Name ? $resolved->toString() : $name->toString(), '\\'); + } + + /** @return list */ + private function resolvedNameCandidates(Node\Name $name): array + { + $names = []; + $resolved = $name->getAttribute('resolvedName'); + if ($resolved instanceof Node\Name) { + $names[] = ltrim($resolved->toString(), '\\'); + } + $fallback = $name->getAttribute('fallbackName'); + if ($fallback instanceof Node\Name) { + $names[] = ltrim($fallback->toString(), '\\'); + } + $names[] = ltrim($name->toString(), '\\'); + return array_values(array_unique($names)); + } } diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 80954c70..13281d98 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -90,8 +90,9 @@ trait ArrayExpressionTrait if ($node->dim === null) { $this->fatalError($node, 'Cannot use [] for GLOBALS'); } - if ($this->isScalarString($node->dim)) { - $name = $node->dim->value; + $staticSlot = $this->getStaticGlobalsSlot($node); + if ($staticSlot !== null) { + $name = $staticSlot; if (!$this->hasGlobalVar($name)) { $this->addGlobalVar($name, Type::VAR); } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 21d33206..7018e6c7 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -275,7 +275,7 @@ trait AssignOpTrait if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); } - } elseif (($globalSlot = $this->getLiteralGlobalsSlot($left)) !== null) { + } elseif (($globalSlot = $this->getStaticGlobalsSlot($left)) !== null) { if (!$this->hasGlobalVar($globalSlot)) { $this->addGlobalVar($globalSlot, Type::VAR); } @@ -1189,7 +1189,7 @@ trait AssignOpTrait // declared as Variant because boxing the raw pointer would coerce it // to bool. Other values retain the normal nullable Variant behavior. $var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; - $globalSlot = $this->getLiteralGlobalsSlot($expr->var); + $globalSlot = $this->getStaticGlobalsSlot($expr->var); if ($globalSlot !== null) { if (!$this->hasGlobalVar($globalSlot)) { $this->addGlobalVar($globalSlot, Type::VAR); @@ -1201,6 +1201,15 @@ trait AssignOpTrait $this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $expr->expr); } $var = $globalSlot; + } elseif ($nativeRight + && $expr->var instanceof Expr\ArrayDimFetch + && $this->isVarExpr($expr->var->var) + && $expr->var->var->name === 'GLOBALS' + ) { + $this->fatalError( + $expr->var, + 'Native objects cannot be stored in dynamically addressed `$GLOBALS`', + ); } if ($var !== null && !$this->hasVar($var)) { if ($nativeRight) { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 06206776..4e78d37c 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -151,7 +151,8 @@ class Preprocessor extends CompilerBase } } - $resolver = new NativeGlobalTypeResolver($this->symbols->classes()); + $resolver = new NativeGlobalTypeResolver($this->symbols->classes(), $this->constants); + $this->nativeGlobalTypeResolver = $resolver; $discovery = new NativeGlobalDiscovery($resolver, $functionReturns); foreach ($candidateSources as $source) { diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index b72f266c..c9a70c22 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -21,14 +21,15 @@ trait DeclarationSymbolTrait if ($this->namespace) { $name = $this->namespace . '\\' . $name; } - $this->addConstant($name, $value); + $this->addConstant($name, $value, $const->value); } } - protected function addConstant(string $name, string $value): void + protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void { $constInfo = new \stdClass(); $constInfo->value = $value; + $constInfo->valueExpr = $valueExpr; $constInfo->type = $this->detectStrValueType($value); $constInfo->namespace = $this->namespace; $constInfo->name = $name; diff --git a/src/Translator.php b/src/Translator.php index c14fcef2..19893c87 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4500,7 +4500,12 @@ CODE; private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void { if ($this->isInternalInterface($interfaceName)) { - $this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName); + // Zend validates ordinary classes while registering their class + // entries. Only Native classes need the compiler-side reflection + // contract because they deliberately have no zend_class_entry. + if ($classDef->nativeObject) { + $this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName); + } return; } if (!$this->hasInterface($interfaceName)) { diff --git a/src/gen_stub.php b/src/gen_stub.php index f8c89bfd..c4503df1 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -3351,14 +3351,9 @@ class PropertyInfo extends VariableLike $template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n"; } - $minimumFlagCompatibility = $this->phpVersionIdMinimumCompatibility; - if (($this->flags & (Modifiers::PRIVATE_SET | Modifiers::PROTECTED_SET)) - && ($minimumFlagCompatibility === null || $minimumFlagCompatibility >= PHP_84_VERSION_ID)) { - $minimumFlagCompatibility = PHP_82_VERSION_ID; - } $code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode( $template, - $minimumFlagCompatibility + $this->phpVersionIdMinimumCompatibility ); $code .= $stringRelease; diff --git a/tests/compiler/native-class/global-and-static.phpt b/tests/compiler/native-class/global-and-static.phpt index 0a3b04df..75ad29b6 100644 --- a/tests/compiler/native-class/global-and-static.phpt +++ b/tests/compiler/native-class/global-and-static.phpt @@ -6,9 +6,13 @@ Native class: TypePHP globals and static locals retain request-rooted native obj #[Native] class NativeCounter { + public const string GLOBAL_KEY = 'nativeGlobalsArray'; + public int $value; } +const NATIVE_GLOBAL_KEY = NativeCounter::GLOBAL_KEY; + #[Native] class NativeCounterChild extends NativeCounter {} @@ -46,13 +50,13 @@ function globalIsNull(): bool function initializeGlobalsArray(): void { - $GLOBALS['nativeGlobalsArray'] ??= new NativeCounter(); - $GLOBALS['nativeGlobalsArray']->value = 42; + $GLOBALS[NATIVE_GLOBAL_KEY] ??= new NativeCounter(); + $GLOBALS[NativeCounter::GLOBAL_KEY]->value = 42; } function readGlobalsArray(): int { - return $GLOBALS['nativeGlobalsArray']->value; + return $GLOBALS[NATIVE_GLOBAL_KEY]->value; } function resetGlobalsArray(): void @@ -62,7 +66,7 @@ function resetGlobalsArray(): void function globalsArrayIsNull(): bool { - return is_null($GLOBALS['nativeGlobalsArray']); + return is_null($GLOBALS[NativeCounter::GLOBAL_KEY]); } function nextStatic(): int