diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 9fa3de40..0b7cf70e 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -240,7 +240,7 @@ final class InvalidContext | `object` | `php::Object` | 保存任意 Zend Object | | Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 | | `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 | -| `mixed` | `php::Var` | 保存任意 PHP zval | +| `mixed` / `any` | `php::Var` | 保存任意 PHP zval;两种声明具有相同的无约束槽语义 | | 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | | `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 | | BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI | @@ -277,7 +277,19 @@ struct php_app__requestcontext final { 允许字段持有 ZendVM 值不代表 Native Class Object 本身进入 ZendVM。ZendVM 可以管理字段指向的 String、Array、Object 或 resource,但它不知道外层 Native Class 的存在。 -### 6.1 初始化状态 +### 6.1 属性引用 + +Native 属性是否允许取引用必须完全由声明元数据在编译期决定,不生成运行时类型分支: + +- `mixed` / `any` 是无约束的 `php::Var` 槽,允许 `$ref =& $object->property`。 +- `bool`、`int`、`float` 等固定布局字段不能表示 PHP 引用,编译期拒绝。 +- `string`、`array`、`object`、Stream 和高精度类型虽然具有 PHPX 包装层,但仍是固定声明类型,引用写入会绕过类型约束,因此编译期拒绝。 +- nullable、union、intersection 等受约束的 `php::Var` 字段同样拒绝引用;不能仅因底层存储也是 `php::Var` 就允许。 +- 带 Property Hook 的属性没有可暴露的实体槽,始终拒绝引用。 + +这项规则只允许引用无约束字段值,不允许引用 Native Object 指针变量本身。Native Object 变量之间的普通赋值已经共享对象身份。 + +### 6.2 初始化状态 Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段增加额外状态位。对象创建时,每个没有显式默认值的字段直接使用类型零值: @@ -293,7 +305,7 @@ Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段 Property Hook 的虚拟属性没有实体字段,但 Hook 声明仍必须包含类型。 -### 6.2 赋值检查 +### 6.3 赋值检查 已确定的赋值在编译期检查。来自 `mixed`、动态 PHP 返回值或其他无法静态确定的值,在写入字段前执行一次运行时类型检查。检查完成后直接写入对应字段,不经过 Zend property handler。 @@ -1212,6 +1224,7 @@ $json = json_encode($nativeObject->toArray()); | nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 | | Native 参数/返回值的 `&` | 不支持;编译期 FatalError | | 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 | +| 对 Native 属性取引用 | 仅显式声明为 `mixed` / `any` 的无约束字段支持;其他字段编译期 FatalError | | Native variadic、union/intersection | 不支持;编译期 FatalError | | `__construct()` | 支持 | | `clone` / `__clone()` | 支持 | diff --git a/examples/xml.php b/examples/xml.php new file mode 100644 index 00000000..d18a391b --- /dev/null +++ b/examples/xml.php @@ -0,0 +1,12 @@ +'); +var_dump($xml); +var_dump((bool)$xml); + +$obj = new stdClass(); +var_dump((bool) $obj); + +class UserClass {} + +$user = new UserClass(); +var_dump((bool) $user); diff --git a/phpunit/code/native-class-any-property-reference.php b/phpunit/code/native-class-any-property-reference.php new file mode 100644 index 00000000..f503e484 --- /dev/null +++ b/phpunit/code/native-class-any-property-reference.php @@ -0,0 +1,14 @@ +value; + $reference = 42; +} diff --git a/phpunit/code/native-class-explicit-destructor-call.php b/phpunit/code/native-class-explicit-destructor-call.php new file mode 100644 index 00000000..fd01f736 --- /dev/null +++ b/phpunit/code/native-class-explicit-destructor-call.php @@ -0,0 +1,15 @@ +__destruct(); +} diff --git a/phpunit/code/native-class-forward/a.php b/phpunit/code/native-class-forward/a.php new file mode 100644 index 00000000..d22e51b8 --- /dev/null +++ b/phpunit/code/native-class-forward/a.php @@ -0,0 +1,12 @@ +value; +} diff --git a/phpunit/code/native-class-property-unset.php b/phpunit/code/native-class-property-unset.php new file mode 100644 index 00000000..a8325b44 --- /dev/null +++ b/phpunit/code/native-class-property-unset.php @@ -0,0 +1,13 @@ +value); +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index c8f29dcc..ab135be7 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -6,6 +6,25 @@ use TypePhp\Exception\TestError; final class NativeClassValidationTest extends \BaseTest { + public function testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing(): void + { + global $translator; + + $compiler = \TypePhp\CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $directory = dirname(__DIR__, 2) . '/code/native-class-forward'; + $files = [$directory . '/a.php', $directory . '/b.php']; + $compiler->discoverNativeClassDeclarations($files); + foreach ($files as $file) { + $compiler->prepareFile($file); + } + foreach ($files as $file) { + $compiler->convertFile($file); + } + + $this->addToAssertionCount(1); + } + public function testRejectsNativeAttributeOnInterface(): void { $this->expectException(\TypePhp\Exception\SyntaxError::class); @@ -244,6 +263,32 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-reference-assignment.php'); } + public function testRejectsReferencesToNativeObjectProperties(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Only Native object properties declared as any or mixed can be referenced'); + $this->compile('native-class-property-reference.php'); + } + + public function testAllowsReferencesToExplicitAnyNativeObjectProperties(): void + { + $this->compile('native-class-any-property-reference.php'); + } + + public function testRejectsUnsetOnNativeObjectProperties(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object properties cannot be unset'); + $this->compile('native-class-property-unset.php'); + } + + public function testRejectsExplicitNativeDestructorCall(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Explicit calls to native object destructors are not supported'); + $this->compile('native-class-explicit-destructor-call.php'); + } + public function testRejectsNativeObjectReferenceKeywordMethod(): void { $this->expectException(TestError::class); diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 966596e6..1e7d19f3 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -135,6 +135,7 @@ trait SourcePipelineTrait } $files = $this->filterIgnoredFiles($files); + $this->discoverNativeClassDeclarations($files); // 分析 PHP 文件,预处理 foreach ($files as $k => $file) { if (FileScanner::isPhpFile($file)) { diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 584e7fec..0fa5da95 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -437,6 +437,8 @@ class CompilerBase implements PropertyAccessContext protected array $globalVars = []; /** @var array Global/static Native pointer slot => class name. */ protected array $nativeGlobalObjects = []; + /** @var array Lowercase class name => declared Native class name. */ + protected array $nativeClassDeclarations = []; /** @var array Request-reset initialization flags for Native static locals. */ protected array $nativeStaticInitializers = []; protected bool $nativeTypes = false; @@ -4060,7 +4062,10 @@ class CompilerBase implements PropertyAccessContext $this->assertExprCanBeUsedAsValue($expr->expr, 'eval operand'); // 对 eval() 指令的 PHP 代码段禁止字面量优化 $expr->expr->setAttribute('noLiteralString', true); - return 'php::eval(' . $this->identifierToStr($expr->expr) . ')'; + $source = $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr)) + ? $this->parseExprToString($expr->expr) + : $this->identifierToStr($expr->expr); + return 'php::eval(' . $source . ')'; } protected function parseInclude(Expr\Include_ $expr): string @@ -4084,7 +4089,9 @@ class CompilerBase implements PropertyAccessContext break; } - $fileName = $this->parseIdentifier($expr->expr); + $fileName = $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr)) + ? $this->parseExprToString($expr->expr) + : $this->parseIdentifier($expr->expr); $scope = []; foreach ($this->context->localVars as $name => $_type) { @@ -4571,7 +4578,19 @@ class CompilerBase implements PropertyAccessContext } $list = []; foreach ($expr->parts as $part) { - $list[] = $this->identifierToStr($part); + if (!$part instanceof Node\InterpolatedStringPart) { + $this->assertExprCanBeUsedAsValue($part, 'shell command interpolation value'); + } + if ($part instanceof Node\InterpolatedStringPart) { + $list[] = $this->parseExpr($part); + } elseif ($this->isNativeObjectClass($this->detectClassOfExpr($part))) { + $list[] = $this->parseOrderedOperand( + new Expr\MethodCall($part, new Node\Identifier('toString')), + false, + ); + } else { + $list[] = $this->parseOrderedOperand($part, false); + } } return 'php::fn::shell_exec(php::concat({' . implode(', ', $list) . '}))'; } diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index 09f7aa7a..79eb96b9 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -18,6 +18,8 @@ class PropertyDef public ?string $default = null; public ?ArrayInitPlan $arrayInitPlan = null; public bool $nullable = false; + /** The declared type is the unconstrained `mixed`/`any` type. */ + public bool $explicitMixed = false; public string $class = ''; public array $typeCheck = []; public string $typeStr = ''; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 7fdc07d8..c891cfa9 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -659,6 +659,13 @@ trait CallArgumentGenerator protected function materializeCallArgValue(NodeAbstract $value, string $expr): string { + // A Native property fetch is a typed C++ pointer, never an INDIRECT + // zval. Passing it through php_deindirect() would box the pointer as a + // bool/Variant and break the Native ABI. Dynamic Zend calls reject the + // value before reaching here; direct Native calls keep it unchanged. + if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) { + return $expr; + } // A call that returns by reference yields a live php::Ref aliasing the // callee's storage. When such a call feeds a by-value argument, PHP takes // a value snapshot at evaluation time (left to right), so later mutations diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 0ac1ee1c..66f9d64f 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -258,7 +258,13 @@ trait NativeClassSupportTrait protected function isNativeObjectClass(string $class): bool { $class = ltrim($class, '\\'); - return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; + if ($class === '') { + return false; + } + if (isset($this->nativeClassDeclarations[strtolower($class)])) { + return true; + } + return $this->hasClass($class) && $this->getClass($class)->nativeObject; } /** @@ -361,8 +367,23 @@ trait NativeClassSupportTrait protected function getNativeObjectCppName(string|ClassDef $class): string { - $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); - return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); + if ($class instanceof ClassDef) { + return self::PREFIX . $this->getNativeName('', $class->namespace, $class->name); + } + $class = ltrim($class, '\\'); + if ($this->hasClass($class)) { + $classDef = $this->getClass($class); + return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); + } + + // The Native declaration catalog is built before semantic + // preprocessing, so signatures may name a Native class declared in a + // later file. Its C++ symbol is derivable from the fully-qualified PHP + // name without requiring the complete ClassDef yet. + $separator = strrpos($class, '\\'); + $namespace = $separator === false ? '' : substr($class, 0, $separator); + $name = $separator === false ? $class : substr($class, $separator + 1); + return self::PREFIX . $this->getNativeName('', $namespace, $name); } protected function getNativeObjectDescriptorName(string|ClassDef $class): string @@ -564,16 +585,44 @@ trait NativeClassSupportTrait } /** - * Native objects already have reference semantics: variables contain a - * typed pointer and assignment copies only that pointer. PHP references - * would alias the pointer slot itself, which has no useful Native ABI - * representation and would make a typed slot possible to rebind through - * an untyped reference. + * Validate a Native reference entirely from compile-time metadata. + * + * A Native object variable is a typed pointer and must never expose its + * pointer slot as a PHP reference. A Native property may expose a reference + * only when it was explicitly declared `mixed`/`any`: that field is an + * unconstrained php::Var slot. Fixed-layout fields and constrained Variant + * fields must reject references because a later reference write could + * bypass their declared type. */ protected function assertNativeObjectReferenceForbidden( NodeAbstract $expr, NodeAbstract $errorNode, ): void { + if ($expr instanceof Node\Expr\PropertyFetch) { + $receiverClass = $this->detectClassOfExpr($expr->var); + if ($this->isNativeObjectClass($receiverClass)) { + if (!$expr->name instanceof Node\Identifier) { + $this->fatalError($errorNode, 'Dynamic native object property access is not supported'); + } + $property = $expr->name->toString(); + $resolution = $this->resolveNativeInstanceProperty($expr, $property, $receiverClass); + if ($resolution === null) { + $this->fatalError( + $errorNode, + "Native class `{$receiverClass}` has no property `\${$property}`", + ); + } + $this->applyNativePropertyAccessResult($expr, $resolution); + $definition = $resolution->propertyDef; + if (!$definition->explicitMixed || $definition->getter !== null || $definition->setter !== null) { + $this->fatalError( + $errorNode, + 'Only Native object properties declared as any or mixed can be referenced', + ); + } + return; + } + } $class = $this->detectDeclaredClassOfExpr($expr); if ($this->isNativeObjectClass($class)) { $this->fatalError( diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index a88898a8..b74d088b 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -246,20 +246,16 @@ trait AssignOpTrait if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); } - } elseif ($left instanceof Expr\PropertyFetch - && $this->isVarExpr($left->var) - && $this->isIdExpr($left->name) - ) { - $receiver = $this->parseVariable($left->var); - if ($this->isNativeObjectVar($receiver)) { - $property = $this->findNativeObjectProperty( - $this->getNativeObjectVarClass($receiver), - $left->name->toString(), - ); - if ($property !== null - && $property->class !== '' - && $this->isInterface($property->class) - ) { + } elseif ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) { + $receiverClass = $this->detectClassOfExpr($left->var); + if ($this->isNativeObjectClass($receiverClass)) { + $propertyName = $left->name->toString(); + $resolution = $this->resolveNativeInstanceProperty($left, $propertyName, $receiverClass); + if ($resolution !== null) { + $this->applyNativePropertyAccessResult($left, $resolution); + } + $property = $resolution?->propertyDef; + if ($property !== null && $property->class !== '' && $this->isInterface($property->class)) { $this->fatalError( $left, 'Native objects cannot be assigned to interface-typed properties', @@ -306,42 +302,46 @@ trait AssignOpTrait $this->fatalError($left, 'Cannot write to read-only hooked property'); } - if ($left instanceof Expr\PropertyFetch - && $this->isVarExpr($left->var) - && $this->isIdExpr($left->name) - ) { - $object = $this->parseIdentifier($left->var); - if ($this->isNativeObjectVar($object)) { - $property = $this->parseIdentifier($left->name); - $class = $this->getNativeObjectVarClass($object); + if ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) { + $receiverClass = $this->detectClassOfExpr($left->var); + if ($this->isNativeObjectClass($receiverClass)) { + $property = $left->name->toString(); $access = $this->getNativePropertyAccess($left); if ($access === null) { - $this->fatalError($left, "Native class `{$class}` has no property `\${$property}`"); + $resolution = $this->resolveNativeInstanceProperty($left, $property, $receiverClass); + if ($resolution === null) { + $this->fatalError($left, "Native class `{$receiverClass}` has no property `\${$property}`"); + } + $this->applyNativePropertyAccessResult($left, $resolution); + $access = $this->getNativePropertyAccess($left); } $def = $access->getPropertyDef(); - $field = $this->getNativeObjectPropertyCppName($def, $access->getClassDef()); - $rightExpr = $this->parseExprAsValue($right); + + // Parse and materialize the receiver before the right-hand + // expression. PHP evaluates an object/property target before + // its assigned value, and C++ operand order must not decide it. + $leftExpr = $this->parsePropertyFetch($left); if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) { if ($this->isNull($right)) { if (!$def->nullable) { - $this->fatalError($right, "Cannot assign null to native property `{$class}::\${$property}`"); + $this->fatalError($right, "Cannot assign null to native property `{$receiverClass}::\${$property}`"); } - return $this->getNativeObjectMemberReceiver($object) - . $field . ' = nullptr'; + return $leftExpr . ' = nullptr'; } $rightClass = $this->detectClassOfExpr($right); if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) { - $this->fatalError($right, "Cannot assign value to native property `{$class}::\${$property}`"); - } - } else { - $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); - $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); - if ($def->type !== Type::VAR) { - $rightExpr = $this->convertExprFromType($def->type, $rightExpr); + $this->fatalError($right, "Cannot assign value to native property `{$receiverClass}::\${$property}`"); } + return $leftExpr . ' = ' . $this->parseExprAsValue($right); + } + + $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); + $rightExpr = $this->parseExprAsValue($right); + $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); + if ($def->type !== Type::VAR) { + $rightExpr = $this->convertExprFromType($def->type, $rightExpr); } - return $this->getNativeObjectMemberReceiver($object) - . $field . ' = ' . $rightExpr; + return $leftExpr . ' = ' . $rightExpr; } } diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 023f1730..8fe58b18 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -207,6 +207,12 @@ trait PropertyAccessTrait protected function emitDynamicPropertyFetchRef(Expr\PropertyFetch $expr, NodeAbstract $errorNode): string { + $receiverClass = $this->detectClassOfExpr($expr->var); + if ($this->isNativeObjectClass($receiverClass)) { + $this->assertNativeObjectReferenceForbidden($expr, $errorNode); + return $this->parsePropertyFetch($expr) . '.toReference()'; + } + // Reference diagnostics are more specific than the generic readonly // mutation error emitted by preparePropertyWriteTarget(). $target = $this->preparePropertyWriteTarget($expr, true); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 3bd3ff38..e79f31c1 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -21,6 +21,7 @@ use TypePhp\Entity\PropertyDef; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; use TypePhp\Exception\SyntaxError; use TypePhp\Transform\PropertyHookLowering; +use TypePhp\Transform\CompileTimeAttribute; use TypePhp\Transform\NativeClassAttributeLowering; use TypePhp\Transform\PrinterLowering; use TypePhp\Transform\ArrayableLowering; @@ -42,6 +43,69 @@ use PhpParser\NodeVisitor\NameResolver; class Preprocessor extends CompilerBase { + /** + * Discover Native class names before parsing any signatures or fields. + * + * PHP permits forward class references across both declaration and file + * order. Native fields need the same property while choosing a concrete + * C++ pointer type, so waiting for prepareClass() would be order-dependent. + * Only files which mention both an attribute and "Native" are parsed in + * this lightweight pass; ordinary projects pay no second parse cost. + * + * @param list $files + */ + public function discoverNativeClassDeclarations(array $files): void + { + foreach ($files as $file) { + if (!$this->isPhpFileForNativeDiscovery($file)) { + continue; + } + $source = file_get_contents($file); + if (!is_string($source) + || !str_contains($source, '#[') + || stripos($source, 'native') === false + ) { + continue; + } + try { + $ast = $this->parser->parse($source); + } catch (\PhpParser\Error) { + // prepareFile() owns the normal source diagnostic, including + // the filename and compiler formatting. Avoid reporting a + // syntax error twice from this declaration-only pass. + continue; + } + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); + $ast = $traverser->traverse($ast); + $this->discoverNativeClassDeclarationsInAst($ast); + } + } + + private function isPhpFileForNativeDiscovery(string $file): bool + { + return str_ends_with(strtolower($file), '.php'); + } + + /** @param array $ast */ + private function discoverNativeClassDeclarationsInAst(array $ast): void + { + $finder = new NodeFinder(); + foreach ($finder->findInstanceOf($ast, Node\Stmt\Class_::class) as $class) { + if ($class->name === null + || (!NativeClassAttributeLowering::isNative($class) + && CompileTimeAttribute::find($class, 'Native') === null) + ) { + continue; + } + $name = isset($class->namespacedName) + ? $class->namespacedName->toString() + : $class->name->toString(); + $name = ltrim($name, '\\'); + $this->nativeClassDeclarations[strtolower($name)] = $name; + } + } + public function getSortedFiles(array $list): array { $sorter = new StringSort(); @@ -158,6 +222,10 @@ class Preprocessor extends CompilerBase $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); + // CompilerTest and embedding users may invoke prepareFile() + // directly instead of the project pipeline. Preserve same-file + // forward Native references for that public entry path as well. + $this->discoverNativeClassDeclarationsInAst($stmts); foreach ($stmts as $v) { $type = $v->getType(); @@ -1288,6 +1356,17 @@ class Preprocessor extends CompilerBase } $propDef = new PropertyDef($name, $flags, $type, $default, $nullable); + if ($typeNode !== null + && !$typeNode instanceof NullableType + && !$typeNode instanceof UnionType + && !$typeNode instanceof IntersectionType + ) { + $propDef->explicitMixed = in_array( + strtolower($this->parseIdentifier($typeNode)), + ['mixed', 'any'], + true, + ); + } $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->class = $class; $propDef->arrayInitPlan = $arrayInitPlan; @@ -1472,7 +1551,7 @@ class Preprocessor extends CompilerBase 'null' => ['null'], 'object' => [], // no literal object default exists 'self', 'parent', 'static' => [], - 'mixed' => null, + 'mixed', 'any' => null, 'callable' => null, // string/array/closure — not checkable default => [], // class type: only null via ?Type }; diff --git a/tests/compiler/native-class/any-property-reference.phpt b/tests/compiler/native-class/any-property-reference.phpt new file mode 100644 index 00000000..6f149056 --- /dev/null +++ b/tests/compiler/native-class/any-property-reference.phpt @@ -0,0 +1,44 @@ +--TEST-- +Native any properties support PHP references without runtime type dispatch +--FILE-- +value; + $reference = 'changed'; + var_dump($object->value); + + $object->value = 42; + var_dump($reference); + + $object->child = new NativeAnyReference(); + $childReference =& $object->child->value; + replaceAny($childReference, ['native', 'reference']); + var_dump($object->child->value); +} + +?> +--EXPECT-- +string(7) "changed" +int(42) +array(2) { + [0]=> + string(6) "native" + [1]=> + string(9) "reference" +} diff --git a/tests/compiler/native-class/include-native-path.inc b/tests/compiler/native-class/include-native-path.inc new file mode 100644 index 00000000..06e39270 --- /dev/null +++ b/tests/compiler/native-class/include-native-path.inc @@ -0,0 +1,3 @@ +right = $right; + $right->left = $left; + + echo roundTripMutual($left->right)->name, ':', $right->left->name, "\n"; +} + +?> +--EXPECT-- +right:left diff --git a/tests/compiler/native-class/nested-property-write.phpt b/tests/compiler/native-class/nested-property-write.phpt new file mode 100644 index 00000000..80e9044e --- /dev/null +++ b/tests/compiler/native-class/nested-property-write.phpt @@ -0,0 +1,40 @@ +--TEST-- +Native class: direct writes support chained and expression receivers +--FILE-- +value = $value; + } +} + +function makeNativeWriteNode(int $value): NativeWriteNode +{ + return new NativeWriteNode($value); +} + +function main(): void +{ + $root = new NativeWriteNode(1); + $root->child = new NativeWriteNode(2); + $leaf = new NativeWriteNode(3); + + $root->child->child = $leaf; + echo $root->child->child->value, "\n"; + + $replacement = new NativeWriteNode(4); + makeNativeWriteNode(5)->child = $replacement; + echo $replacement->value, "\n"; +} + +?> +--EXPECT-- +3 +4 diff --git a/tests/compiler/native-class/shell-exec-conversion.phpt b/tests/compiler/native-class/shell-exec-conversion.phpt new file mode 100644 index 00000000..8151f19c --- /dev/null +++ b/tests/compiler/native-class/shell-exec-conversion.phpt @@ -0,0 +1,29 @@ +--TEST-- +Native class: shell command interpolation uses the declared toString method +--SKIPIF-- + +--FILE-- + +--EXPECT-- +native-shell diff --git a/tests/compiler/native-class/string-language-operands.phpt b/tests/compiler/native-class/string-language-operands.phpt new file mode 100644 index 00000000..07371bbe --- /dev/null +++ b/tests/compiler/native-class/string-language-operands.phpt @@ -0,0 +1,34 @@ +--TEST-- +Native class: include and eval operands use the declared toString method +--FILE-- +value = $value; + } + + public function toString(): string + { + return $this->value; + } +} + +function main(): void +{ + $path = new NativeStringOperand(__DIR__ . '/include-native-path.inc'); + include $path; + + $source = new NativeStringOperand('echo "evaluated\\n";'); + eval($source); +} + +?> +--EXPECT-- +included +evaluated