From de8fdaf4645b64809bfde63659c81cc2d5717de3 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 17 Aug 2026 14:36:30 +0800 Subject: [PATCH] =?UTF-8?q?Native=20Class=20=E6=94=AF=E6=8C=81=20[?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E9=98=B6=E6=AE=B5]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NATIVE_CLASS_OBJECT.md | 48 ++++- .../code/native-class-array-access-isset.php | 10 + .../code/native-class-array-access-unset.php | 10 + .../code/native-class-array-access-write.php | 10 + phpunit/code/native-class-array-access.php | 10 + phpunit/code/native-class-array-dim-key.php | 11 + phpunit/code/native-class-array-key.php | 10 + phpunit/code/native-class-box-property.php | 7 + .../native-class-coalesce-assign-type.php | 18 ++ .../native-class-coalesce-property-type.php | 19 ++ .../code/native-class-compound-assignment.php | 10 + .../native-class-dynamic-class-constant.php | 13 ++ phpunit/code/native-class-dynamic-new.php | 10 + .../code/native-class-dynamic-static-call.php | 13 ++ phpunit/code/native-class-foreach.php | 14 ++ .../code/native-class-generator-factory.php | 19 ++ phpunit/code/native-class-generator-local.php | 14 ++ .../code/native-class-generator-method.php | 11 + .../code/native-class-generator-parameter.php | 13 ++ phpunit/code/native-class-generator-yield.php | 17 ++ phpunit/code/native-class-increment.php | 10 + .../code/native-class-reflection-class.php | 12 ++ .../code/native-class-reflection-member.php | 18 ++ phpunit/code/native-class-switch-case.php | 13 ++ phpunit/code/native-class-switch.php | 13 ++ phpunit/code/native-class-throw.php | 13 ++ phpunit/code/native-class-unary-operator.php | 10 + phpunit/code/native-class-variable-method.php | 14 ++ .../code/native-class-variable-property.php | 14 ++ .../NativeClass/NativeClassValidationTest.php | 196 ++++++++++++++++++ src/CompilerBase.php | 52 ++++- src/Generator/FiberGenerator.php | 14 ++ src/NativeClass/NativeClassSupportTrait.php | 150 ++++++++++++++ src/Parser/ArrayExpressionTrait.php | 4 + src/Parser/AssignOpTrait.php | 82 +++++++- src/Parser/BinaryOpTrait.php | 7 + src/Parser/ClassConstantFetchTrait.php | 3 + src/Parser/ExceptionControlFlowTrait.php | 4 +- src/Parser/ForeachTrait.php | 6 + src/Parser/MethodCallTrait.php | 3 + src/Parser/PropertyAccessTrait.php | 1 + src/Parser/SelectionExpressionTrait.php | 39 +++- src/Parser/SwitchTrait.php | 10 + src/Parser/UnaryExpressionTrait.php | 3 + src/Preprocessor.php | 12 ++ src/Translator.php | 2 +- tests/compiler/native-class/conditions.phpt | 17 ++ .../native-class/exit-conversion.phpt | 22 ++ .../compiler/native-class/fiber-lifetime.phpt | 43 ++++ .../compiler/native-class/fiber-shutdown.phpt | 40 ++++ tests/compiler/native-class/gc-threshold.phpt | 28 +++ .../native-class/include-native-scope.inc | 4 + .../native-class/include-native-scope.phpt | 19 ++ .../native-class/keyword-conversions.phpt | 5 + .../compiler/native-class/match-identity.phpt | 60 ++++++ .../native-class/strict-identity.phpt | 46 ++++ .../native-class/value-selection.phpt | 27 +++ 57 files changed, 1287 insertions(+), 16 deletions(-) create mode 100644 phpunit/code/native-class-array-access-isset.php create mode 100644 phpunit/code/native-class-array-access-unset.php create mode 100644 phpunit/code/native-class-array-access-write.php create mode 100644 phpunit/code/native-class-array-access.php create mode 100644 phpunit/code/native-class-array-dim-key.php create mode 100644 phpunit/code/native-class-array-key.php create mode 100644 phpunit/code/native-class-box-property.php create mode 100644 phpunit/code/native-class-coalesce-assign-type.php create mode 100644 phpunit/code/native-class-coalesce-property-type.php create mode 100644 phpunit/code/native-class-compound-assignment.php create mode 100644 phpunit/code/native-class-dynamic-class-constant.php create mode 100644 phpunit/code/native-class-dynamic-new.php create mode 100644 phpunit/code/native-class-dynamic-static-call.php create mode 100644 phpunit/code/native-class-foreach.php create mode 100644 phpunit/code/native-class-generator-factory.php create mode 100644 phpunit/code/native-class-generator-local.php create mode 100644 phpunit/code/native-class-generator-method.php create mode 100644 phpunit/code/native-class-generator-parameter.php create mode 100644 phpunit/code/native-class-generator-yield.php create mode 100644 phpunit/code/native-class-increment.php create mode 100644 phpunit/code/native-class-reflection-class.php create mode 100644 phpunit/code/native-class-reflection-member.php create mode 100644 phpunit/code/native-class-switch-case.php create mode 100644 phpunit/code/native-class-switch.php create mode 100644 phpunit/code/native-class-throw.php create mode 100644 phpunit/code/native-class-unary-operator.php create mode 100644 phpunit/code/native-class-variable-method.php create mode 100644 phpunit/code/native-class-variable-property.php create mode 100644 tests/compiler/native-class/exit-conversion.phpt create mode 100644 tests/compiler/native-class/fiber-lifetime.phpt create mode 100644 tests/compiler/native-class/fiber-shutdown.phpt create mode 100644 tests/compiler/native-class/gc-threshold.phpt create mode 100644 tests/compiler/native-class/include-native-scope.inc create mode 100644 tests/compiler/native-class/include-native-scope.phpt create mode 100644 tests/compiler/native-class/match-identity.phpt create mode 100644 tests/compiler/native-class/strict-identity.phpt diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 263087f9..9fa3de40 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -328,11 +328,17 @@ b->x = 10; Native Class Object 不使用 `std::shared_ptr`。`std::shared_ptr` 的控制块、原子引用计数和循环引用问题与本特性的极致性能目标不符。 Native Object 的严格比较使用指针身份:`===`/`!==` 判断两个槽是否指向同一个 Native -对象,也支持与 `null` 比较。PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native +对象,也支持与 `null` 比较。与任何 Zend 标量或 Zend Object 的严格比较恒为 +`false`,不会把裸指针隐式转换为 `bool`。`match` 条件同样使用这一指针身份规则。 +PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native Object 没有 Zend object handler,而且对象图可能包含环,因此不提供隐式字段值比较。 松散比较、大小比较和算术/位运算在编译期直接报错。需要值相等语义时应声明一个具有 明确字段和循环处理规则的普通 Native 方法。 +一元算术/位运算、`++`/`--`、复合算术赋值和 `switch` 也依赖 PHP 的数值或松散比较 +语义,因此禁止用于 Native Object。该检查必须发生在 C++ 生成前,避免裸指针意外进入 +合法但危险的 C++ 指针算术。 + `isset($native)` 与 `empty($native)` 直接检查裸指针是否为 `nullptr`。命名 Native 属性链使用短路 lambda 逐级检查中间指针,不会把指针传入 `php::Variant`,因此 `isset($node->next->next)` 在中间槽为空时返回 `false`,而不是触发空对象调用。 @@ -789,6 +795,28 @@ nullable class 必须使用 `?Point`,使用相同指针表示,`nullptr` 表 参数和返回值必须显式声明具体 Native Class(或其 nullable 形式),不能通过 `mixed`、 `object` 或 Interface carrier 传递。 +例如: + +```php +function bar(Point $point): void +{ + // 函数入口已经完成非空检查;进入函数体后 $point 一定指向 Point 对象。 + echo $point->x; +} + +function maybeBar(?Point $point): void +{ + // $point 可能是空指针,使用前必须先收窄或由成员访问生成空值检查。 + if ($point !== null) { + echo $point->x; + } +} +``` + +两者的 C++ ABI 都使用 `php_app__point *`,但契约不同:`bar()` 在执行第一条用户 +语句前拒绝 `nullptr`;`maybeBar()` 接受 `nullptr`。这项入口保证只约束传入时的值, +函数内部仍可把自己的局部指针槽重新赋为 `null`,且不会改变调用者的变量槽。 + ## 10. ZendVM 边界 Native Object 没有对应的 `zval` 表示,因此只能传给明确接受相同 Native Class或其 Native 基类的参数。Interface 只用于校验 Native Class 的声明契约,不能作为 Native Object 的参数、属性、变量或返回值 carrier。 @@ -803,6 +831,12 @@ Native Class 的字段可以保存 `php::Var`、`php::Array` 或 `php::Object` - 使用 `$nativeObject->$expr()`、`$nativeObject->{$expr}()` 等变量方法名调用。 - 放入普通 PHP `array`。 - 捕获到需要注册为 Zend Closure 的闭包中。 +- 作为 TypePHP Generator 的参数、`this`、局部变量、返回类型或 `yield` 值。Generator + 由 Zend Closure/Fiber 状态机表示,Native pointer 不进入该 Zend 状态。 +- 作为 Fiber API 的传入值、恢复值或 Closure capture。普通 TypePHP 函数可以在自己的 + C++ 局部槽中保存 Native Object 并跨越 `Fiber::suspend()`;Native Root Frame 使用 + 可任意 O(1) 摘除的 thread-local 双向侵入链表,GC 会同时扫描运行中与挂起 Fiber 的 + 有效 frame,不依赖跨 Fiber 的 LIFO 析构顺序。 - 作为 `call_user_func()` 等动态 callback 的 receiver。 - 保存到 ZendVM 全局变量或对象属性中。 @@ -1118,6 +1152,13 @@ Native Class 支持 `toArray()`、`toString()`、`toInt()`、`toFloat()`、`toBo 方法返回类型必须与关键词类型完全一致。例如 `toArray(): array`、`toInt(): int`、`toString(): string`;缺少方法、接收参数、按引用返回或返回类型不同均为编译期 FatalError。 +对象条件与显式转换是两套语义。`if ($object)`、`!$object`、`$left && $right` 和 +`$left || $right` 只判断 Native pointer 是否为 `nullptr`,不会调用 `toBool()`;这与 +PHP 对普通对象“存在即为 true”的语义一致,也使 nullable Native pointer 可以直接作为 +条件。只有显式 `(bool) $object` 或 `$object->toBool()` 才解析为 Native `toBool(): bool` +调用;没有定义该方法时在编译期报错。即使类定义的 `toBool()` 返回 `false`,一个非空 +对象在 `if ($object)` 中仍为 `true`。 + `__toString(): string` 是 `toString(): string` 的兼容别名。对 Native Object 使用 `toString()`、`strval($object)`、`(string) $object`、字符串拼接或 `echo` 时,编译器优先使用实际声明的 `toString()`,若不存在则使用 `__toString()`。 与 PHP 一致,声明合法 `__toString()` 的 Native Class 在编译期隐式满足 `Stringable`; @@ -1193,7 +1234,9 @@ $json = json_encode($nativeObject->toArray()); | Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 | | `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 | | `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 | +| Native 条件的 `match` | 支持,使用与 `===` 相同的指针身份规则 | | `==` / `!=`、大小及算术/位运算 | 不支持,编译期 FatalError;值相等应使用显式 Native 方法 | +| 一元算术/位运算、`++`/`--`、复合算术赋值、`switch` | 不支持,编译期 FatalError | | 动态属性 | 不支持 | | `$nativeObject->$expr()` | 不支持,只允许命名方法调用 | | `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 | @@ -1201,6 +1244,8 @@ $json = json_encode($nativeObject->toArray()); | `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 | | `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler | | Reflection | 不支持 | +| TypePHP Generator 保存或产出 Native Object | 不支持;编译期 FatalError | +| 普通函数的 Native 局部变量跨 `Fiber::suspend()` | 支持;Root Frame 注册表允许非 LIFO Fiber 生命周期 | | `get_class()` / `get_parent_class()` / `get_called_class()` | 不支持 Native runtime introspection;使用 `self::class`、`parent::class` 或具体类名 | | WeakReference | 不支持 | | PHP serialize | 不支持 | @@ -1208,6 +1253,7 @@ $json = json_encode($nativeObject->toArray()); | 动态 callback | 不支持 | | 动态 PHP/eval 使用 | 不支持 | | 普通 PHP array 保存 Native Object | 不支持 | +| Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError | | Box/Std Container 属性 | 不支持 | | Box 保存 Native Object | 不支持 | | 局部 Std Container 保存 Native Object | 支持具体 Native class value type;容器 Root Frame 参与 GC tracing | diff --git a/phpunit/code/native-class-array-access-isset.php b/phpunit/code/native-class-array-access-isset.php new file mode 100644 index 00000000..7f2068f5 --- /dev/null +++ b/phpunit/code/native-class-array-access-isset.php @@ -0,0 +1,10 @@ + 'value']; +} diff --git a/phpunit/code/native-class-box-property.php b/phpunit/code/native-class-box-property.php new file mode 100644 index 00000000..42373654 --- /dev/null +++ b/phpunit/code/native-class-box-property.php @@ -0,0 +1,7 @@ +value ??= new NativeCoalescePropertyWrong(); +} diff --git a/phpunit/code/native-class-compound-assignment.php b/phpunit/code/native-class-compound-assignment.php new file mode 100644 index 00000000..613d0ce4 --- /dev/null +++ b/phpunit/code/native-class-compound-assignment.php @@ -0,0 +1,10 @@ +value; +} + diff --git a/phpunit/code/native-class-generator-local.php b/phpunit/code/native-class-generator-local.php new file mode 100644 index 00000000..bc1e0d13 --- /dev/null +++ b/phpunit/code/native-class-generator-local.php @@ -0,0 +1,14 @@ +value; +} + diff --git a/phpunit/code/native-class-generator-method.php b/phpunit/code/native-class-generator-method.php new file mode 100644 index 00000000..5bad1bf7 --- /dev/null +++ b/phpunit/code/native-class-generator-method.php @@ -0,0 +1,11 @@ +value; +} + diff --git a/phpunit/code/native-class-generator-yield.php b/phpunit/code/native-class-generator-yield.php new file mode 100644 index 00000000..f56e0b7f --- /dev/null +++ b/phpunit/code/native-class-generator-yield.php @@ -0,0 +1,17 @@ +value; + } +} + +function main(): void +{ + new ReflectionMethod(NativeReflectionMemberValue::class, 'read'); +} + diff --git a/phpunit/code/native-class-switch-case.php b/phpunit/code/native-class-switch-case.php new file mode 100644 index 00000000..0f397687 --- /dev/null +++ b/phpunit/code/native-class-switch-case.php @@ -0,0 +1,13 @@ +$method(); +} diff --git a/phpunit/code/native-class-variable-property.php b/phpunit/code/native-class-variable-property.php new file mode 100644 index 00000000..0655f076 --- /dev/null +++ b/phpunit/code/native-class-variable-property.php @@ -0,0 +1,14 @@ +$property); +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index a29db234..c8f29dcc 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -69,6 +69,13 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-std-container-property.php'); } + public function testRejectsBoxProperty(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class properties cannot use Box types'); + $this->compile('native-class-box-property.php'); + } + public function testRejectsNativeStdContainerConversionToPhpArray(): void { $this->expectException(TestError::class); @@ -125,6 +132,27 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-instanceof.php'); } + public function testRejectsNativeObjectAsDynamicNewTarget(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as dynamic class targets'); + $this->compile('native-class-dynamic-new.php'); + } + + public function testRejectsNativeObjectAsDynamicStaticCallTarget(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as dynamic class targets'); + $this->compile('native-class-dynamic-static-call.php'); + } + + public function testRejectsNativeObjectAsDynamicClassConstantTarget(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as dynamic class targets'); + $this->compile('native-class-dynamic-class-constant.php'); + } + public function testRejectsNativeObjectStoredInPhpArray(): void { $this->expectException(TestError::class); @@ -328,6 +356,20 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-dynamic-magic-method.php'); } + public function testRejectsVariableNativeMethodCalls(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Dynamic native object method calls are not supported'); + $this->compile('native-class-variable-method.php'); + } + + public function testRejectsVariableNativePropertyAccess(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Dynamic native object property access is not supported'); + $this->compile('native-class-variable-property.php'); + } + public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void { $this->expectException(TestError::class); @@ -426,6 +468,97 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-arithmetic-operator.php'); } + public function testRejectsNativeUnaryOperators(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support the unary `-` operator'); + $this->compile('native-class-unary-operator.php'); + } + + public function testRejectsIncrementingNativePointerSlots(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support the `++` operator'); + $this->compile('native-class-increment.php'); + } + + public function testRejectsNativeCompoundAssignments(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support the `+=` operator'); + $this->compile('native-class-compound-assignment.php'); + } + + public function testRejectsIncompatibleNativeCoalesceAssignment(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Cannot assign native object `NativeCoalesceWrong` to `NativeCoalesceExpected`'); + $this->compile('native-class-coalesce-assign-type.php'); + } + + public function testRejectsIncompatibleNativePropertyCoalesceAssignment(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Cannot assign object of class `NativeCoalescePropertyWrong` to object property `value` of class `NativeCoalescePropertyExpected`'); + $this->compile('native-class-coalesce-property-type.php'); + } + + public function testRejectsNativeSwitchConditions(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as switch values'); + $this->compile('native-class-switch.php'); + } + + public function testRejectsNativeSwitchCaseValues(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as switch values'); + $this->compile('native-class-switch-case.php'); + } + + public function testRejectsNativeArrayLiteralKeys(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as PHP array keys'); + $this->compile('native-class-array-key.php'); + } + + public function testRejectsNativeArrayDimensionKeys(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be used as PHP array keys'); + $this->compile('native-class-array-dim-key.php'); + } + + public function testRejectsArrayAccessOnNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->compile('native-class-array-access.php'); + } + + public function testRejectsArrayWritesOnNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->compile('native-class-array-access-write.php'); + } + + public function testRejectsArrayIssetOnNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->compile('native-class-array-access-isset.php'); + } + + public function testRejectsArrayUnsetOnNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->compile('native-class-array-access-unset.php'); + } + public function testRejectsInaccessibleNativeCloneMethod(): void { $this->expectException(TestError::class); @@ -524,4 +657,67 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-null-return.php'); } + public function testRejectsNativeObjectGeneratorParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Generator functions cannot accept, capture, or return Native objects'); + $this->compile('native-class-generator-parameter.php'); + } + + public function testRejectsNativeObjectGeneratorMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Generator functions cannot accept, capture, or return Native objects'); + $this->compile('native-class-generator-method.php'); + } + + public function testRejectsNativeObjectConstructionInsideGenerator(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be created inside Generator functions'); + $this->compile('native-class-generator-local.php'); + } + + public function testRejectsNativeObjectRetainedByGeneratorFromFactory(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Generator functions cannot retain Native objects across suspension'); + $this->compile('native-class-generator-factory.php'); + } + + public function testRejectsYieldingNativeObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be yielded through a Zend Generator'); + $this->compile('native-class-generator-yield.php'); + } + + public function testRejectsNativeClassReflection(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class `NativeReflectionClassValue` cannot be used with ReflectionClass'); + $this->compile('native-class-reflection-class.php'); + } + + public function testRejectsNativeMemberReflection(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class `NativeReflectionMemberValue` cannot be used with ReflectionMethod'); + $this->compile('native-class-reflection-member.php'); + } + + public function testRejectsThrowingNativeObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be thrown as Zend exceptions'); + $this->compile('native-class-throw.php'); + } + + public function testRejectsIteratingNativeObjectThroughZendForeach(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be iterated by PHP foreach'); + $this->compile('native-class-foreach.php'); + } + } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index b7171895..584e7fec 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1480,6 +1480,7 @@ class CompilerBase implements PropertyAccessContext protected function parseArrayKey(NodeAbstract $expr): string { + $this->assertNotNativeObjectArrayKey($expr); $key = $this->parseIdentifier($expr); if (str_starts_with($key, self::LITERAL_STRINGS)) { $key = "{$key}.str()"; @@ -3179,6 +3180,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreInc(Expr\PreInc $expr): string { + $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '++'); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, '+', true); @@ -3566,6 +3568,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string { + $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, str_repeat($op, 2)); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, $op, false); @@ -3615,6 +3618,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreDec(Expr\PreDec $expr): string { + $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '--'); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, '-', true); @@ -3633,6 +3637,9 @@ class CompilerBase implements PropertyAccessContext protected function parsePrint(Expr\Print_ $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'print operand'); + if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))) { + return 'php::print(' . $this->parseExprToString($expr->expr) . ')'; + } return 'php::print(' . $this->parseExprAsValue($expr->expr) . ')'; } @@ -3667,6 +3674,9 @@ class CompilerBase implements PropertyAccessContext protected function parseNew(Expr\New_ $expr): string { + if (!$expr->class instanceof Node\Stmt\Class_ && !$this->isNameExpr($expr->class)) { + $this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr); + } $ctorClassName = ''; // 匿名类 if ($expr->class instanceof Node\Stmt\Class_) { @@ -3719,6 +3729,7 @@ class CompilerBase implements PropertyAccessContext $className = $this->getNamespacedClassName($className); } $ctorClassName = $className; + $this->assertNativeClassNotUsedWithReflection($expr, $className); if ($this->isAbstractClass($className)) { $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); } @@ -3732,6 +3743,14 @@ class CompilerBase implements PropertyAccessContext ); } if ($this->isNativeObjectClass($className)) { + if ($this->inGeneratorBody) { + // Generator lowering deliberately keeps Native + // values out of the Zend Closure/Fiber state. + $this->fatalError( + $expr, + 'Native objects cannot be created inside Generator functions', + ); + } $cppClass = $this->getNativeObjectCppName($className); $descriptor = $this->getNativeObjectDescriptorName($className); if ($constructor === null) { @@ -3909,9 +3928,16 @@ class CompilerBase implements PropertyAccessContext // Although C++17 orders the braced-list elements, materializing an // expression prevents captured statements from a later part from // being hoisted ahead of an earlier Call. - $list[] = $part instanceof Node\InterpolatedStringPart - ? $this->parseExpr($part) - : $this->parseOrderedOperand($part, false); + 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::concat({' . implode(', ', $list) . '})'; @@ -3957,7 +3983,9 @@ class CompilerBase implements PropertyAccessContext if (!$node->expr) { return 'php::aotExit()'; } - $status = $this->parseExprAsValue($node->expr); + $status = $this->isNativeObjectClass($this->detectClassOfExpr($node->expr)) + ? $this->parseExprToString($node->expr) + : $this->parseExprAsValue($node->expr); return 'php::aotExit(' . $status . ')'; } @@ -4063,6 +4091,13 @@ class CompilerBase implements PropertyAccessContext if ($name === 'this_' || str_starts_with($name, 'tmp_var_')) { continue; } + // Native pointers have no zval representation. Boxing one into + // the include symbol table would silently select a bool overload + // and expose `true` instead of the object. Dynamic PHP is not + // allowed to observe Native locals, so leave these names absent. + if ($this->isNativeObjectVar($name)) { + continue; + } $phpName = $this->unescapeVarName($name); $scope[] = '{ ' . $this->getLiteralString($phpName) . '.str(), php::Var(' . $name . ') }'; } @@ -4879,10 +4914,11 @@ class CompilerBase implements PropertyAccessContext if ($this->context->nativeObjects !== []) { $rootSlots = []; foreach ($this->context->nativeObjects as $name => $_class) { - if ($name === 'this_') { - $code .= $this->getIndent() . 'auto *_native_this_root = &this_;' . PHP_EOL; - $rootSlots[] = 'reinterpret_cast(&_native_this_root)'; - } elseif ($this->hasLocalVar($name)) { + // `this_` and Native parameters are borrowed from a generated + // caller which already owns a root slot (nativeConstruct/ + // nativeClone do the same for lifecycle callbacks). Only + // function-owned pointer slots must be registered here. + if ($name !== 'this_' && !$this->hasArgument($name) && $this->hasLocalVar($name)) { $rootSlots[] = 'reinterpret_cast(&' . $name . ')'; } } diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 6466647c..bd9bfa9a 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -185,6 +185,11 @@ trait FiberGenerator private function materializeYieldOperand(Node $expr, bool $force = false): string { + if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) { + // Yield payloads are stored in a Zend array and cross the Fiber / + // Generator object boundary. A Native pointer has no zval form. + $this->fatalError($expr, 'Native objects cannot be yielded through a Zend Generator'); + } [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); foreach ($beforeStmts as $stmt) { $this->context->beforeStmtLines[] = $stmt; @@ -286,6 +291,15 @@ trait FiberGenerator if ($v->stmts) { $body .= $this->parseStmts($v->stmts); } + if ($this->context->nativeObjects !== []) { + // TypePHP Generators are represented by Zend Closure/Fiber state. + // Keep Native values out of that generated state even though the + // runtime root registry itself now tolerates Fiber suspension. + $this->fatalError( + $v, + 'Generator functions cannot retain Native objects across suspension', + ); + } $body .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL; if ($this->context->needsUserCodeCallableScope) { $body = $this->genUserCodeCallableScopeGuard() . $body; diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index ab31e892..0ac1ee1c 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -315,6 +315,50 @@ trait NativeClassSupportTrait $this->fatalError($expr, "Native objects do not support the `{$operator}` operator{$suffix}"); } + /** + * A Native object expression is a raw C++ pointer. Applying arithmetic + * unary or update operators to it could otherwise become pointer + * arithmetic, which is valid C++ but has no PHP object semantics. + */ + protected function assertNativeObjectOperatorOperandSupported( + NodeAbstract $operand, + NodeAbstract $errorNode, + string $operator, + bool $unary = false, + ): void { + if (!$this->isNativeObjectClass($this->detectClassOfExpr($operand))) { + return; + } + + $prefix = $unary ? 'unary ' : ''; + $this->fatalError($errorNode, "Native objects do not support the {$prefix}`{$operator}` operator"); + } + + protected function assertNotNativeObjectArrayKey(NodeAbstract $key): void + { + if ($this->isNativeObjectClass($this->detectClassOfExpr($key))) { + $this->fatalError($key, 'Native objects cannot be used as PHP array keys'); + } + } + + protected function assertNotNativeObjectArrayDimensionReceiver( + NodeAbstract $receiver, + NodeAbstract $errorNode, + ): void { + if ($this->isNativeObjectClass($this->detectClassOfExpr($receiver))) { + $this->fatalError($errorNode, 'Native objects do not support array dimension access'); + } + } + + protected function assertNotNativeObjectDynamicClassTarget( + NodeAbstract $target, + NodeAbstract $errorNode, + ): void { + if ($this->isNativeObjectClass($this->detectClassOfExpr($target))) { + $this->fatalError($errorNode, 'Native objects cannot be used as dynamic class targets'); + } + } + protected function getNativeObjectCppName(string|ClassDef $class): string { $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); @@ -539,6 +583,68 @@ trait NativeClassSupportTrait } } + /** + * Reflection constructors accept a class name string, so the ordinary + * Native-pointer Zend boundary check cannot see this escape. Reject known + * Native class literals before generating a lookup for a class which is + * intentionally absent from the Zend class table. + */ + protected function assertNativeClassNotUsedWithReflection( + Node\Expr\New_ $expr, + string $constructedClass, + ): void { + $reflectionClass = strtolower(ltrim($constructedClass, '\\')); + if (!in_array($reflectionClass, [ + 'reflectionclass', + 'reflectionmethod', + 'reflectionproperty', + 'reflectionclassconstant', + 'reflectionenum', + 'reflectionenumunitcase', + 'reflectionenumbackedcase', + ], true) || $expr->args === []) { + return; + } + + $target = $expr->args[0]->value; + $nativeClass = ''; + if ($this->isScalarString($target)) { + // Reflection string arguments are runtime names, not names relative + // to the current PHP namespace. + $candidate = ltrim($target->value, '\\'); + if ($this->isNativeObjectClass($candidate)) { + $nativeClass = $candidate; + } + } elseif ($this->isClassConstFetch($target) + && $this->isNameExpr($target->class) + && $this->isIdExpr($target->name) + && strtolower($this->parseIdentifier($target->name)) === 'class' + ) { + $candidate = $this->parseIdentifier($target->class); + if ($candidate === 'self' || $candidate === 'static') { + $candidate = $this->getFullClassName(); + } elseif ($candidate === 'parent') { + $candidate = $this->classDef?->extends ?? ''; + } else { + $candidate = $this->getNamespacedClassName($candidate); + } + if ($this->isNativeObjectClass($candidate)) { + $nativeClass = $candidate; + } + } + + if ($nativeClass !== '') { + $reflectionName = strrchr($constructedClass, '\\'); + $reflectionName = $reflectionName === false + ? $constructedClass + : substr($reflectionName, 1); + $this->fatalError( + $target, + "Native class `{$nativeClass}` cannot be used with {$reflectionName}", + ); + } + } + protected function getNativeObjectReturnType(FunctionDef $function): ?string { if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) { @@ -614,6 +720,50 @@ trait NativeClassSupportTrait return isset($this->context->nonNullNativeObjects[$name]); } + /** + * Conservative straight-line non-null proof for a Native pointer value. + * This deliberately excludes properties (a non-nullable Native field has + * a nullptr zero value) and control-flow expressions. The caller may only + * retain the proof at function top level, where no branch merge is needed. + */ + protected function isNativeObjectExpressionKnownNonNull(NodeAbstract $expr): bool + { + if ($expr instanceof Node\Expr\ErrorSuppress) { + return $this->isNativeObjectExpressionKnownNonNull($expr->expr); + } + if ($expr instanceof Node\Expr\New_ || $expr instanceof Node\Expr\Clone_) { + return $this->isNativeObjectClass($this->detectClassOfExpr($expr)); + } + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + return $this->isNativeObjectKnownNonNull($this->parseIdentifier($expr)); + } + if ($expr instanceof Node\Expr\FuncCall + && ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name)) + ) { + $native = $this->findNativeFunction($this->parseIdentifier($expr->name)); + if ($native !== false) { + $function = $this->getFunction($native); + return $this->isNativeObjectClass($function->returnClass) + && !$function->returnNullable; + } + } + if ($expr instanceof Node\Expr\MethodCall + && $this->isIdExpr($expr->name) + ) { + $receiverClass = $this->detectClassOfExpr($expr->var); + if ($this->isNativeObjectClass($receiverClass)) { + $method = $this->findNativeObjectMethod( + $receiverClass, + $this->parseIdentifier($expr->name), + ); + return $method !== null + && $this->isNativeObjectClass($method->functionDef->returnClass) + && !$method->functionDef->returnNullable; + } + } + return false; + } + protected function getNativeObjectReceiver(string $name): string { if ($name === 'this_') { diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index e8f8032b..0837d9a1 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -170,6 +170,10 @@ trait ArrayExpressionTrait protected function parseArrayDimFetch(Expr\ArrayDimFetch $node): string { + $this->assertNotNativeObjectArrayDimensionReceiver($node->var, $node); + if ($node->dim !== null) { + $this->assertNotNativeObjectArrayKey($node->dim); + } $write = $this->isArrayDimFetchUpdate($node); if ($this->isStdContainerExpr($node)) { if ($write && $node->dim === null) { diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index d551a3c0..a88898a8 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -21,6 +21,12 @@ trait AssignOpTrait { protected function parseAssignArrayDim(NodeAbstract $left, NodeAbstract $right): string { + if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) { + $this->assertNotNativeObjectArrayKey($left->dim); + } + if ($left instanceof Expr\ArrayDimFetch) { + $this->assertNotNativeObjectArrayDimensionReceiver($left->var, $left); + } if ($this->isPropertyFetch($left)) { return $this->parseAssignPropertyArrayDim($left, $right); } @@ -288,6 +294,7 @@ trait AssignOpTrait $finalVarType = $this->getNormalAssignType($type); $runtimeObjectAssignClass = ''; $assigningNullToTypedObject = false; + $markNativeObjectNonNull = false; if ($type === Type::VOID) { $type = Type::VAR; } @@ -358,6 +365,11 @@ trait AssignOpTrait } // 类型推断,获取对象的类名,如果不是对象则返回空字符串 $rightClass = $this->detectClassOfExpr($right); + $markNativeObjectNonNull = $this->context->scopeLevel <= 1 + && !$this->hasScopeGlobalVar($var) + && !$this->hasStaticVar($var) + && $this->isNativeObjectClass($rightClass) + && $this->isNativeObjectExpressionKnownNonNull($right); if ($this->isNativeObjectVar($var)) { // Assignment rebinds the local pointer slot. Even an // assignment nested in a conditional invalidates the simple @@ -520,6 +532,12 @@ trait AssignOpTrait . ' return php::toObject(' . $checkedValue . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ');' . ' })(' . $rightExpr . ')'; } + if ($markNativeObjectNonNull) { + // Parsing is sequential: this proof affects only subsequent source + // expressions. Nested control flow is excluded above because its + // assignment may not execute on every path. + $this->markNativeObjectNonNull($var); + } $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); if ($propertyWriteTarget !== null && ($propertyDef = $this->getNativePropertyDef($left)) !== null) { @@ -611,6 +629,7 @@ trait AssignOpTrait protected function parseAssignOp(Expr\AssignOp $node, string $op): string { + $this->assertNativeObjectOperatorOperandSupported($node->var, $node, $op); $this->assertNotNullsafeWriteContext($node->var); $this->assertNativePropertyHookDirectWriteTarget($node->var); $pythonOperator = $this->parsePythonAssignOperator($node); @@ -1077,11 +1096,34 @@ trait AssignOpTrait { $this->checkLeftValue($expr->var); + $rightClass = $this->detectClassOfExpr($expr->expr); + $nativeRight = $this->isNativeObjectClass($rightClass); + // An undefined variable must exist before generating its isset check. - // Keep it as Variant so NULL remains distinguishable from native defaults. + // A Native RHS establishes a typed nullptr slot; it must never be + // 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; if ($var !== null && !$this->hasVar($var)) { - $this->addLocalVar($var, Type::VAR); + if ($nativeRight) { + $this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); + $this->addNativeObject($var, $rightClass); + } else { + $this->addLocalVar($var, Type::VAR); + } + } + if ($var !== null && $this->isNativeObjectVar($var)) { + $leftClass = $this->getNativeObjectVarClass($var); + if ($this->isNull($expr->expr)) { + // nullptr remains a valid nullable slot value. + } elseif (!$nativeRight) { + $this->fatalError($expr->expr, "Native object `\${$var}` cannot be converted to var/object"); + } elseif (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { + $this->fatalError( + $expr->expr, + "Cannot assign native object `{$rightClass}` to `{$leftClass}`", + ); + } } $isset = $this->parseChainedExpr($expr->var, self::OP_ISSET); @@ -1093,13 +1135,49 @@ trait AssignOpTrait $this->assertCanAssignPropertyWrite($propertyWriteTarget, $expr->expr); } + $rightBeforeCount = count($this->context->beforeStmtLines); + $rightAfterCount = count($this->context->afterStmtLines); $right = $this->parseExpr($expr->expr); + $rightBefore = array_slice($this->context->beforeStmtLines, $rightBeforeCount); + $rightAfter = array_slice($this->context->afterStmtLines, $rightAfterCount); + $this->context->beforeStmtLines = array_slice( + $this->context->beforeStmtLines, + 0, + $rightBeforeCount, + ); + $this->context->afterStmtLines = array_slice( + $this->context->afterStmtLines, + 0, + $rightAfterCount, + ); if ($propertyWriteTarget !== null) { $right = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $expr->expr, $right); } if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) { $this->errorUndefinedVariable($expr->expr); } + $targetClass = $var !== null && $this->isNativeObjectVar($var) + ? $this->getNativeObjectVarClass($var) + : $this->detectClassOfExpr($expr->var); + if (($rightBefore !== [] || $rightAfter !== []) && $this->isNativeObjectClass($targetClass)) { + $tmp = $this->genTmpVarName(); + $pointerType = $this->getNativeObjectPointerType($targetClass); + $this->addLocalVar($tmp, $pointerType); + $this->addNativeObject($tmp, $targetClass); + $code = '[&]() -> ' . $pointerType . ' {' . PHP_EOL; + $code .= $this->getIndent() . 'if (' . $isset . ') { return ' . $var . '; }' . PHP_EOL; + $code .= $this->formatCapturedStmtLines($rightBefore); + $code .= $this->getIndent() . $tmp . ' = ' . $right . ';' . PHP_EOL; + $code .= $this->formatCapturedStmtLines($rightAfter); + $code .= $this->getIndent() . $var . ' = ' . $tmp . ';' . PHP_EOL; + $code .= $this->getIndent() . 'return ' . $var . ';' . PHP_EOL; + $code .= $this->getIndent() . '}()'; + return $code; + } + $this->appendCapturedStmtLinesToContext($rightBefore); + foreach ($rightAfter as $stmt) { + $this->context->afterStmtLines[] = $stmt; + } return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))'; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 7eacf2f5..cef4b1a4 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -810,6 +810,13 @@ trait BinaryOpTrait if ($leftIsNative && $rightIsNative) { return 'static_cast(' . $left . ') == static_cast(' . $right . ')'; } + if ($leftIsNative || $rightIsNative) { + // A Native pointer has no zval representation and can never be + // strictly identical to a Zend value. Explicit void casts retain + // both PHP side effects in left-to-right order without allowing + // C++ to coerce the pointer to bool. + return '(static_cast(' . $left . '), static_cast(' . $right . '), false)'; + } if ($right === 'nullptr') { // The left operand may itself be an assignment or another compound // expression. Parenthesize it before invoking Variant::isNull(), or diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index 57a7ff0d..194dfb9b 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -17,6 +17,9 @@ trait ClassConstantFetchTrait { protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string { + if (!$this->isNameExpr($expr->class)) { + $this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr); + } $this->rejectPythonModuleClassConstantFetch($expr); if (!$this->isIdExpr($expr->name)) { diff --git a/src/Parser/ExceptionControlFlowTrait.php b/src/Parser/ExceptionControlFlowTrait.php index 561b623d..ab73f16a 100644 --- a/src/Parser/ExceptionControlFlowTrait.php +++ b/src/Parser/ExceptionControlFlowTrait.php @@ -22,6 +22,9 @@ trait ExceptionControlFlowTrait if ($this->method === '__destruct') { $this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak"); } + if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))) { + $this->fatalError($expr, 'Native objects cannot be thrown as Zend exceptions'); + } $type = $this->detectTypeOfExpr($expr->expr); if ($this->isNewExpr($expr->expr)) { $ex = $this->parseExpr($expr->expr); @@ -237,4 +240,3 @@ trait ExceptionControlFlowTrait } } - diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 9b1db1d5..1fc889a5 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -171,6 +171,12 @@ trait ForeachTrait protected function parseForeach(Foreach_ $node): string { + if ($this->isNativeObjectClass($this->detectClassOfExpr($node->expr))) { + // Native classes have no Zend iterator handlers and cannot carry + // Traversable as a runtime interface value. Expose iteration via + // an explicitly returned PHP array or a typed std container. + $this->fatalError($node->expr, 'Native objects cannot be iterated by PHP foreach'); + } if ($this->isVarExpr($node->expr)) { $name = $this->parseIdentifier($node->expr); if ($this->hasVar($name)) { diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 804b492b..4d1e19d6 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -749,6 +749,9 @@ trait MethodCallTrait protected function parseStaticCall(Expr\StaticCall $expr): string { + if (!$this->isNameExpr($expr->class)) { + $this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr); + } $pythonCall = $this->parsePythonModuleStaticCall($expr); if ($pythonCall !== null) { return $pythonCall; diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 68db2891..023f1730 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -782,6 +782,7 @@ trait PropertyAccessTrait $this->assertNotNullsafeWriteContext($var); $this->assertNativePropertyHookDirectWriteTarget($var); if ($this->isArrayDimFetch($var)) { + $this->assertNotNativeObjectArrayDimensionReceiver($var->var, $var); if ($var->dim === null) { $this->fatalError($var, 'Cannot use [] for array unset'); } diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index c9a6bd61..29ff0afb 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -124,6 +124,8 @@ trait SelectionExpressionTrait { $nativeClass = $this->detectClassOfExpr($expr); $nativeSelection = $this->isNativeObjectClass($nativeClass); + $conditionNativeClass = $this->detectClassOfExpr($expr->cond); + $nativeCondition = $this->isNativeObjectClass($conditionNativeClass); $returnType = $nativeSelection ? $this->getNativeObjectPointerType($nativeClass) : Type::VAR; @@ -134,7 +136,13 @@ trait SelectionExpressionTrait $this->errorUndefinedVariable($expr->cond); } } else { - $tmpVar = $this->addTmpVar(Type::VAR); + if ($nativeCondition) { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($conditionNativeClass)); + $this->addNativeObject($tmpVar, $conditionNativeClass); + } else { + $tmpVar = $this->addTmpVar(Type::VAR); + } $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $var . ';'; $var = $tmpVar; } @@ -157,12 +165,37 @@ trait SelectionExpressionTrait $code .= $this->getIndent() . 'if (!' . $matched . ') {'; $code .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { - $condTmpVar = $this->addTmpVar(Type::VAR); + $condClass = $this->detectClassOfExpr($cond); + if ($nativeCondition && $this->isNativeObjectClass($condClass)) { + $condTmpVar = $this->genTmpVarName(); + $this->addLocalVar($condTmpVar, $this->getNativeObjectPointerType($condClass)); + $this->addNativeObject($condTmpVar, $condClass); + } else { + $condTmpVar = $this->addTmpVar(Type::VAR); + } $code .= $this->getIndent() . "{$condTmpVar} = {$condValue};"; $code .= $this->formatCapturedStmtLines($afterStmts); $condValue = $condTmpVar; } - $code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');'; + if ($nativeCondition) { + $condClass = $this->detectClassOfExpr($cond); + if ($this->isNull($cond)) { + $comparison = $var . ' == nullptr'; + } elseif ($this->isNativeObjectClass($condClass) + && ($this->isObjectClassStaticallyAssignableTo($condClass, $conditionNativeClass) + || $this->isObjectClassStaticallyAssignableTo($conditionNativeClass, $condClass)) + ) { + $comparison = $var . ' == ' . $condValue; + } else { + // PHP match uses strict identity. An unrelated Native + // pointer or a Zend value can never be identical, but + // its expression must still be evaluated for effects. + $comparison = '(static_cast(' . $condValue . '), false)'; + } + $code .= $this->getIndent() . $matched . ' = (' . $comparison . ');'; + } else { + $code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');'; + } $code .= $this->getIndent() . '}'; } $code .= $this->getIndent() . 'if (' . $matched . ') {'; diff --git a/src/Parser/SwitchTrait.php b/src/Parser/SwitchTrait.php index 293cf324..08d3dedf 100644 --- a/src/Parser/SwitchTrait.php +++ b/src/Parser/SwitchTrait.php @@ -16,6 +16,16 @@ trait SwitchTrait protected function parseSwitch(Node\Stmt\Switch_ $v): string { $cond = $v->cond; + if ($this->isNativeObjectClass($this->detectClassOfExpr($cond))) { + $this->fatalError($cond, 'Native objects cannot be used as switch values'); + } + foreach ($v->cases as $case) { + if ($case->cond !== null + && $this->isNativeObjectClass($this->detectClassOfExpr($case->cond)) + ) { + $this->fatalError($case->cond, 'Native objects cannot be used as switch values'); + } + } $tmp_var = $this->genTmpVarName(); $type = $this->detectTypeOfExpr($cond); $this->assertExprCanBeUsedAsValue($cond, 'switch condition'); diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 6d7dc76e..e50f108b 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -20,6 +20,7 @@ trait UnaryExpressionTrait if ($pythonOperator !== null) { return $pythonOperator; } + $this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '~', true); $type = $this->detectTypeOfExpr($expr->expr); $this->assertExprCanBeUsedAsValue($expr->expr, 'bitwise operand'); if ($type === Type::BIGINT) { @@ -89,6 +90,7 @@ trait UnaryExpressionTrait if ($pythonOperator !== null) { return $pythonOperator; } + $this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '-', true); $type = $this->detectTypeOfExpr($expr->expr); $this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand'); if ($type === Type::BIGFLOAT) { @@ -124,6 +126,7 @@ trait UnaryExpressionTrait if ($pythonOperator !== null) { return $pythonOperator; } + $this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '+', true); $this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand'); return $this->parseExprAsValue($expr->expr); } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 5bcd190c..3bd3ff38 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -663,6 +663,18 @@ class Preprocessor extends CompilerBase $this->parseParams($v->params, $functionDef); $this->assertNativeObjectFunctionSignature($v, $functionDef); + if ($functionDef->generator + && ($this->classDef?->nativeObject || $this->functionUsesNativeObject($functionDef)) + ) { + // FiberGenerator lowers the body to a Zend Closure and captures all + // parameters through php::Args. Native pointers deliberately have + // no zval representation, and a Native method's `this` cannot be + // bound to that Closure either. + $this->fatalError( + $v, + 'Generator functions cannot accept, capture, or return Native objects', + ); + } if ($this->classDef !== null && !$this->classDef->nativeObject diff --git a/src/Translator.php b/src/Translator.php index 97e4393d..3df83744 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1188,8 +1188,8 @@ CODE; $code .= << --EXPECT-- @@ -31,3 +46,5 @@ bool(false) bool(false) bool(true) present +bool(false) +object truthiness diff --git a/tests/compiler/native-class/exit-conversion.phpt b/tests/compiler/native-class/exit-conversion.phpt new file mode 100644 index 00000000..e06a90f1 --- /dev/null +++ b/tests/compiler/native-class/exit-conversion.phpt @@ -0,0 +1,22 @@ +--TEST-- +Native class: exit converts through the native string method +--FILE-- + +--EXPECT-- +native exit diff --git a/tests/compiler/native-class/fiber-lifetime.phpt b/tests/compiler/native-class/fiber-lifetime.phpt new file mode 100644 index 00000000..223ecd38 --- /dev/null +++ b/tests/compiler/native-class/fiber-lifetime.phpt @@ -0,0 +1,43 @@ +--TEST-- +Native class: root frames remain valid across non-LIFO Fiber suspension +--FILE-- +name = $name; + } +} + +function suspendedNativeFrame(): void +{ + $value = new NativeFiberValue('suspended'); + Fiber::suspend(); + echo $value->name, "\n"; +} + +function resumeFromNewerNativeFrame(Fiber $fiber): void +{ + $value = new NativeFiberValue('newer'); + $fiber->resume(); + echo $value->name, "\n"; +} + +function main(): void +{ + $fiber = new Fiber(static function (): void { + suspendedNativeFrame(); + }); + $fiber->start(); + resumeFromNewerNativeFrame($fiber); +} + +?> +--EXPECT-- +suspended +newer diff --git a/tests/compiler/native-class/fiber-shutdown.phpt b/tests/compiler/native-class/fiber-shutdown.phpt new file mode 100644 index 00000000..5e97435e --- /dev/null +++ b/tests/compiler/native-class/fiber-shutdown.phpt @@ -0,0 +1,40 @@ +--TEST-- +Native class: request shutdown safely detaches roots owned by suspended Fibers +--FILE-- +name = $name; + } +} + +function suspendUntilRequestShutdown(): void +{ + $value = new NativeFiberShutdownValue('alive'); + Fiber::suspend(); + + // This frame intentionally remains suspended until request shutdown. + echo $value->name, "\n"; +} + +function main(): void +{ + global $suspendedFiber; + + $suspendedFiber = new Fiber(static function (): void { + suspendUntilRequestShutdown(); + }); + $suspendedFiber->start(); + + echo "suspended\n"; +} + +?> +--EXPECT-- +suspended diff --git a/tests/compiler/native-class/gc-threshold.phpt b/tests/compiler/native-class/gc-threshold.phpt new file mode 100644 index 00000000..1ce2d4c3 --- /dev/null +++ b/tests/compiler/native-class/gc-threshold.phpt @@ -0,0 +1,28 @@ +--TEST-- +Native class: long-running allocation triggers automatic tracing collection +--FILE-- + 0); +} + +?> +--EXPECT-- +bool(true) diff --git a/tests/compiler/native-class/include-native-scope.inc b/tests/compiler/native-class/include-native-scope.inc new file mode 100644 index 00000000..5ee9caf6 --- /dev/null +++ b/tests/compiler/native-class/include-native-scope.inc @@ -0,0 +1,4 @@ + +--EXPECT-- +bool(false) +bool(true) diff --git a/tests/compiler/native-class/keyword-conversions.phpt b/tests/compiler/native-class/keyword-conversions.phpt index f031df45..45121278 100644 --- a/tests/compiler/native-class/keyword-conversions.phpt +++ b/tests/compiler/native-class/keyword-conversions.phpt @@ -57,6 +57,9 @@ function main(): void var_dump((bool) $value); var_dump((string) $value); var_dump(strval($value)); + print $value; + echo "\n"; + echo "interpolated={$value}\n"; $magic = new NativeMagicString(); var_dump($magic->toString()); @@ -81,5 +84,7 @@ float(7.5) bool(true) string(7) "value=7" string(7) "value=7" +value=7 +interpolated=value=7 string(5) "magic" string(5) "magic" diff --git a/tests/compiler/native-class/match-identity.phpt b/tests/compiler/native-class/match-identity.phpt new file mode 100644 index 00000000..3082fd2d --- /dev/null +++ b/tests/compiler/native-class/match-identity.phpt @@ -0,0 +1,60 @@ +--TEST-- +Native class: match compares object identity without entering ZendVM +--FILE-- + 'other', + $same => 'same', + null => 'null', + }; +} + +function main(): void +{ + $value = new NativeMatchValue(); + $other = new NativeMatchValue(); + + var_dump(choose($value, $value, $other)); + var_dump(choose(null, $value, $other)); + var_dump(match (nativeMatchIdentity($value)) { + unrelatedMatchValue() => 'unrelated', + scalarMatchValue() => 'scalar', + $value => 'identity', + }); +} + +?> +--EXPECT-- +string(4) "same" +string(4) "null" +subject +unrelated +scalar +string(8) "identity" diff --git a/tests/compiler/native-class/strict-identity.phpt b/tests/compiler/native-class/strict-identity.phpt new file mode 100644 index 00000000..f2a05599 --- /dev/null +++ b/tests/compiler/native-class/strict-identity.phpt @@ -0,0 +1,46 @@ +--TEST-- +Native class: strict identity never coerces raw pointers to PHP values +--FILE-- + +--EXPECT-- +bool(true) +bool(false) +bool(false) +bool(true) +native +zend +bool(false) +zend +native +bool(false) diff --git a/tests/compiler/native-class/value-selection.phpt b/tests/compiler/native-class/value-selection.phpt index fcc0249c..b64aa340 100644 --- a/tests/compiler/native-class/value-selection.phpt +++ b/tests/compiler/native-class/value-selection.phpt @@ -7,6 +7,7 @@ Native class: ternary, match and coalesce preserve native pointer types class NativeSelectedValue { public int $value; + public ?NativeSelectedValue $child; public function __construct(int $value) { @@ -27,6 +28,17 @@ function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue return $value ?? new NativeSelectedValue(30); } +function makeSelectedValue(): NativeSelectedValue +{ + echo "made\n"; + return new NativeSelectedValue(70); +} + +function identitySelectedValue(NativeSelectedValue $value): NativeSelectedValue +{ + return $value; +} + function main(): void { $first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2); @@ -34,6 +46,18 @@ function main(): void var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value); var_dump(selectWithCoalesce(null)->value); var_dump(selectWithCoalesce($first)->value); + + $created ??= new NativeSelectedValue(40); + $created ??= new NativeSelectedValue(41); + var_dump($created->value); + + $holder = new NativeSelectedValue(50); + $holder->child ??= new NativeSelectedValue(60); + var_dump($holder->child->value); + + $existing = new NativeSelectedValue(80); + $existing ??= identitySelectedValue(makeSelectedValue()); + var_dump($existing->value); } ?> --EXPECT-- @@ -42,3 +66,6 @@ int(10) int(20) int(30) int(1) +int(40) +int(60) +int(80)