diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 2da5131f..263087f9 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -1,7 +1,7 @@ # Native Class Object 设计与实现 > 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、 -> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、单继承、有限虚分派和 +> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派和 > Interface 编译期契约已经落地;本文同时记录尚未开放的边界。 ## 1. 背景 @@ -70,6 +70,8 @@ class Child extends Base {} ``` `#[Native]` 是 Native Class Object 的正式显式声明方式。未使用该注解的普通 class 继续进入现有 ZendVM Object 编译流程。 +该注解只能用于具名 `class`;Interface、Trait 与 Enum 均不能声明为 Native。Trait 仍可由 +Native Class 在 convert 阶段注入,但 Trait 自身不形成 Native runtime 类型。 ## 5. 生成的 C++ 结构 @@ -123,14 +125,21 @@ Native 方法不生成 Zend method wrapper,也不注册到 ZendVM。普通 PHP Native Class 使用 C++ public single inheritance 保持基类子对象布局。PHP 方法的实现主体仍然是 `php_*` 自由函数,不改为复杂的 C++ 成员函数模型。 -全程序分析发现继承链中存在同名的 public/protected instance method 时,为该方法族生成内部 virtual dispatch thunk: +Native abstract class 及 abstract method 也完全在编译期实现。抽象方法在 C++ struct 中 +生成 pure virtual thunk,具体 Native 子类的实现继续调用对应 `php_*` 自由函数;通过抽象 +基类 typed parameter 调用时只发生一次 C++ 虚分派,不注册 Zend class 或 method。 + +全程序分析发现继承链中存在同名的 public/protected instance method 时,为每一个声明层级 +生成独立的内部 virtual slot。子类实现同时覆盖祖先 slot,并以各 slot 自己的参数和返回 +签名生成 adapter,再转调子类的 `php_*` 实现。这样可以保留 PHP 允许的参数逆变、返回值 +协变,而不会把不兼容的 C++ 函数指针或引用强制转换到同一个 vtable slot: ```cpp struct php_app__base; php::Str php_app__base__name(php_app__base &this_); struct php_app__base { - virtual php::Str __native_dispatch_name() { + virtual php::Str __native_dispatch_base_name() { return php_app__base__name(*this); } @@ -141,7 +150,7 @@ struct php_app__child; php::Str php_app__child__name(php_app__child &this_); struct php_app__child : public php_app__base { - php::Str __native_dispatch_name() override { + php::Str __native_dispatch_base_name() override { return php_app__child__name(*this); } }; @@ -155,6 +164,15 @@ struct php_app__child : public php_app__base { - private、static、constructor 和 destructor 不加入 virtual method family。 - PHP 不支持按参数签名重载;Native Class 同样不增加 C++ overload 语义。 - override 必须通过现有 PHP 方法兼容性规则和 Interface 检查。 +- 普通值参数通过 per-declaration adapter 支持 PHP 的参数逆变与返回协变。Native Object + 参数禁止 `&`;typed pointer 按值传递已经共享对象身份,而 `&` 还会暴露调用方指针槽的 + 重绑定能力,这不属于 Native Object ABI。 +- C++ 默认参数由 receiver 的静态类型绑定,不能直接放在 virtual 声明上。编译器为每个 + 可用的位置参数数量生成一个重载 virtual slot;动态选中的 adapter 再调用自身 `php_*` + 实现,因此使用动态实现类的默认值,不需要运行时 presence mask。 +- Native virtual method 的命名参数调用可以省略尾部连续的 optional 参数;若在后续实参 + 之前留下命名参数空洞则编译期拒绝。该少见形状无法由位置型 C++ 重载表达,支持它会使 + 每次虚调用携带 presence mask。非虚 Native Call 仍保留普通命名参数行为。 这会提供继承所必需的有限单分派多态,但不支持变量方法名、运行时 overload resolution、`__call()` 或 ZendVM 动态调用。C++ 编译器仍可对 `final` class、`final` method 和已知精确类型完成去虚化。 @@ -223,8 +241,9 @@ final class InvalidContext | Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 | | `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 | | `mixed` | `php::Var` | 保存任意 PHP zval | -| union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | -| BigInt/BigFloat/Decimal | 对应 PHPX 高精度类型 | 直接使用已有 RAII 类型 | +| 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | +| `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 | +| BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI | `string`、`array`、Zend Object、Stream 和 mixed 字段仍然直接位于 C++ `struct` 的固定偏移处。它们持有的底层 zval 或 zend 对象由 PHPX RAII 类型管理,但属性读取不需要属性哈希表、object handler 或 ZendVM 分派。 @@ -308,6 +327,16 @@ b->x = 10; Native Class Object 不使用 `std::shared_ptr`。`std::shared_ptr` 的控制块、原子引用计数和循环引用问题与本特性的极致性能目标不符。 +Native Object 的严格比较使用指针身份:`===`/`!==` 判断两个槽是否指向同一个 Native +对象,也支持与 `null` 比较。PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native +Object 没有 Zend object handler,而且对象图可能包含环,因此不提供隐式字段值比较。 +松散比较、大小比较和算术/位运算在编译期直接报错。需要值相等语义时应声明一个具有 +明确字段和循环处理规则的普通 Native 方法。 + +`isset($native)` 与 `empty($native)` 直接检查裸指针是否为 `nullptr`。命名 Native +属性链使用短路 lambda 逐级检查中间指针,不会把指针传入 `php::Variant`,因此 +`isset($node->next->next)` 在中间槽为空时返回 `false`,而不是触发空对象调用。 + ### 7.1 Native Class 循环引用 两个或多个 Native Class 可以在属性类型上相互引用: @@ -357,9 +386,9 @@ Native Class 属性始终保存指针,不按值嵌入另一个 Native struct Native Heap tracing GC 能够遍历所有 Native 指针字段,因此 A 与 B 相互指向不会形成引用计数循环,也不会产生永久泄漏。裸指针字段本身没有析构动作。 -但如果循环中的每一条边都是 non-nullable,并且都要求在各自构造函数返回前完成初始化,就会形成无法构造的初始化死结:创建 A 需要 B,而创建 B 又需要 A。 - -首版不引入“未初始化对象发布”或特殊的两阶段构造 API。循环对象图必须至少使用一条 nullable 边打破初始化环: +Native Object 属性的零值是 `nullptr`,包括源码中声明为 non-nullable 的 Native Class +属性。non-nullable 约束只作用于后续显式赋值,不引入 PHP typed property 的 UNDEF +状态。因此循环对象图可以先分别构造,再建立双向关系: ```php $a = new A(); @@ -367,7 +396,7 @@ $b = new B($a); $a->b = $b; ``` -如果编译器发现一个 Native Class 构造依赖环全部由必须在构造阶段赋值的 non-nullable 属性组成,应抛出 FatalError,并提示将至少一条边声明为 nullable。该检查针对构造初始化依赖,而不是简单禁止类型循环。 +不需要构造依赖 SCC 或“两阶段发布”机制;类型 SCC 仅用于 C++ 前置声明与生成顺序。 ## 8. 内存与生命周期 @@ -377,7 +406,7 @@ Native Class Object 应使用独立的、非移动、精确 tracing GC。本文 ### 8.1 Native Heap -Native Heap 使用 Arena/chunk/free-list 提供快速内存分配,但每个对象都具有位于 struct 之前的隐藏 GC header: +首版 Wren 派生实现为每个对象分配一块连续内存,并在 struct 前放置隐藏 GC header: ```cpp struct NativeGcHeader { @@ -393,6 +422,9 @@ auto *point = native_heap.make(); GC header 不属于生成的 C++ struct,也不会改变属性偏移。用户可见对象变量仍然只是一个 `native_struct *`。 +当前分配器使用独立 non-moving allocation;Arena/chunk/free-list 可以作为后续分配器 +优化,但不得改变对象地址稳定性、header 布局、精确 tracing 或 finalization 语义。 + Native Heap 具有以下特征: - non-moving:对象地址从创建到回收始终不变。 @@ -427,7 +459,7 @@ static void trace_a(void *ptr, NativeMarkVisitor &visitor) { `php::Str`、`php::Array`、`php::Object`、`php::Var` 和 Stream 字段由 Zend 引用计数管理,但它们不能反向保存 Native Object,因此无需由 Native GC 深入扫描。 -禁止 Native Object 进入 PHP Array、Box、Std Container 和 Zend Object,是保证 Native 对象图封闭且可精确遍历的重要条件。 +禁止 Native Object 进入 PHP Array、Box 和 Zend Object,是保证 Native 对象图封闭且可精确遍历的重要条件。局部 Std Container 是例外:当元素类型明确写成 Native Class 时,容器直接保存 typed Native pointer,并由独立的容器 Root Frame 在 GC 标记阶段遍历其当前元素。该 Root Frame 跟踪容器而不是元素地址,因此 vector/map 扩容搬迁不会产生悬空 root。 ### 8.3 Root 管理 @@ -461,7 +493,6 @@ struct FunctionNativeRoots { 初版只在确定的 safe point 执行 GC: - Native Heap 分配量超过自适应阈值。 -- Native 对象数量超过阈值。 - Request Shutdown 强制清理全部对象。 Native GC 不暴露语言级显式收集函数。PHPX 内部的收集入口只供运行时阈值策略和底层测试使用,不注册为 TypePHP/PHP API。 @@ -734,17 +765,16 @@ void php_move(php_app__point *point, php::Float x); - 修改属性对调用者可见。 - 在函数内部重新赋值 `$point` 不影响调用者变量。 -引用参数生成二级指针引用: +这里不需要、也不允许 PHP 引用符号: ```php -function replace(Point &$point): void; +function replace(Point &$point): void; // FatalError +$alias =& $point; // FatalError +refval($point); // FatalError +$point->toRef(); // FatalError ``` -近似生成: - -```cpp -void php_replace(php_app__point *&point); -``` +普通的 `$alias = $point` 已经只复制有类型的指针,二者指向并修改同一个对象。 返回 Native Object 时返回指针: @@ -752,7 +782,12 @@ void php_replace(php_app__point *&point); php_app__point *php_create_point(); ``` -非 nullable class 参数在函数入口执行一次空指针检查。确定非空的成员访问不应重复检查。nullable class 使用相同指针表示,`nullptr` 表示 `null`。 +非 nullable class 参数在函数入口执行一次空指针检查。确定非空的成员访问不应重复检查。 +nullable class 必须使用 `?Point`,使用相同指针表示,`nullptr` 表示 `null`。 +`Point $value = null` 这种隐式 nullable 声明不支持,必须写为 `?Point $value = null`。 +`Point|null`、其他 union/intersection、Native variadic 参数以及 Native 引用返回均不支持。 +参数和返回值必须显式声明具体 Native Class(或其 nullable 形式),不能通过 `mixed`、 +`object` 或 Interface carrier 传递。 ## 10. ZendVM 边界 @@ -771,7 +806,10 @@ Native Class 的字段可以保存 `php::Var`、`php::Array` 或 `php::Object` - 作为 `call_user_func()` 等动态 callback 的 receiver。 - 保存到 ZendVM 全局变量或对象属性中。 -Box 和 Std Container 不能保存 Native Object,也不能作为 Native Class 属性。普通 PHP array 同样不能保存 Native Object。 +Box 不能保存 Native Object。Std Container 不能作为 Native Class 属性,但局部 +`std::array`、`std::vector`、`std::map` 和 `std::ordered_map` 可以使用具体 +`NativeClass::class` 作为 value type,并保存该类或其 Native 子类。普通 PHP array +仍然不能保存 Native Object。 任何跨越 ZendVM 边界的行为都应在编译期抛出 FatalError。编译器不得静默装箱或降级,因为这会使性能模型不可预测。 @@ -879,6 +917,8 @@ ZendVM;普通 PHP class 实现该 Interface 的行为不变。Native Class 支 - 使用与 PHP 一致的规则检查 required method 和 hooked property 是否存在、visibility、 static/引用/variadic、参数与返回类型及属性读写约束是否兼容。 +- 项目 Interface 使用预处理得到的完整声明;PHP 内置 Interface 使用 Reflection 得到的 + 正式签名。Tentative return type 保持 PHP 8.4 的非致命语义,不擅自升级为 FatalError。 - 在 Trait AST 注入及继承成员合并完成后检查,因此 Trait 或父类提供的方法可以满足 Interface。 - 支持 Interface 继承和多个 `implements` 声明。 - 不为 Native Class 生成 Interface vtable、runtime interface id、`zend_class_entry` 或 @@ -953,22 +993,19 @@ php_app__user__get_name(*user); php_app__user__set_name(*user, value); ``` -复合写入必须显式展开,并严格保持 PHP 从左到右的求值顺序: +为保持 Native 分支简单且不存在隐式运行时分派,首版只支持直接读取和直接赋值: ```php -$user->count += getValue(); -``` - -近似生成: - -```cpp -auto tmp_value = getValue(); -auto tmp_current = php_app__user__get_count(*user); -php_app__user__set_count(*user, tmp_current + tmp_value); +$value = $user->count; +$user->count = getValue(); ``` 带 Hook 的属性禁止: +- `+=`、`.=` 等复合写入。 +- `++`、`--`。 +- `$object->hookedArray[] = ...`、元素赋值或元素 `unset()` 等间接写入。 +- `isset()`、`empty()`。 - 取引用。 - 引用返回。 - 返回底层属性 slot。 @@ -981,6 +1018,10 @@ Native Property Hook 只有编译期语义,不生成 Zend Property Hook 元数 `clone` 可以支持,但必须由编译器生成字段级浅复制,不能无条件依赖 C++ 默认 copy constructor。 +当 receiver 的静态类型可能保存 Native 子类时,继承层次生成一个内部协变 virtual clone +thunk,由动态子类执行正确尺寸的字段复制并调用其 `__clone()`;不能按静态基类复制,否则会 +发生 C++ object slicing。没有 Native 继承关系的类继续使用静态 clone 路径,不增加 vptr。 + ```php $copy = clone $source; ``` @@ -1001,7 +1042,12 @@ php_app__user____clone(*copy); - PHP Array 保持 PHP 的 copy-on-write 行为,不进行无条件深拷贝。 - Zend Object 字段复制对象句柄,继续指向同一 Zend 对象。 - Native Object 字段复制指针,继续指向同一对象,保持浅复制语义。 -- 完成字段复制后调用可选的 `__clone()`。 +- 完成字段复制后调用可选的 `__clone()`;子类未重新声明时会解析并调用继承的 + `__clone()`,子类重新声明时不会隐式再调用父实现,与普通方法覆盖规则一致。 +- `__clone()` 的 public/protected/private 可见性在 clone 表达式所在的编译期作用域检查, + 不允许通过直接 Native Call 绕过。 +- clone operand 可以是 typed variable、Native function/method call 或 Native property + expression;非变量 operand 会先物化为精确 root 的临时裸指针。 包含不可复制字段的 Native Class 必须显式禁止 clone;对它使用 `clone` 时编译期报错。 @@ -1018,6 +1064,9 @@ php_app__user____construct(*object, args...); 构造函数抛出异常时,必须销毁已经初始化的字段,并从 Native Heap 活动对象集合中移除该对象。 +与其他 TypePHP class 一致,`__construct()` 只能由 `new` 触发。显式调用 +`$object->__construct()` 在编译期报错,避免重复初始化已经存活的 Native Object。 + ### 15.2 析构 `__destruct()` 与 PHP 的精确析构时机存在冲突。Tracing GC 只能保证在对象变为不可达并完成 GC 后执行资源清理,不能保证在最后一个变量离开作用域时立即执行。 @@ -1049,6 +1098,10 @@ struct NativeTypeDescriptor { 5. 未复活对象调用 `destroy()`;实际 C++ destructor 只负责字段 RAII 和基类子对象清理,保持 `noexcept`。descriptor 已记录最派生类型,因此不依赖通过基类指针执行 `delete`,也不要求仅为销毁而给所有继承层次增加 vtable。 6. finalizer 抛出异常时,GC 必须先恢复内部状态并保证对象最终可清理,再把异常传播到当前 TypePHP 异常边界;shutdown 阶段遵循单独的不可抛出策略。 +Request shutdown 会在 finalization 前后各清空一次已注册的 global/static Native root。 +这是必要的:`__destruct()` 可能在 finalization 中把 `$this` 重新写入某个全局槽,但 request +heap 随后仍会整体销毁;第二次清空可防止悬空指针进入下一 request。 + 这种设计保留 `__destruct()` 的资源清理能力,同时避免让复杂用户代码穿过 C++ destructor。它与 PHP 的主要差异是调用时机由 Native GC 决定,而不是引用计数降为零的时刻。 ### 15.3 `unset()` 与析构时机 @@ -1067,7 +1120,32 @@ Native Class 支持 `toArray()`、`toString()`、`toInt()`、`toFloat()`、`toBo `__toString(): string` 是 `toString(): string` 的兼容别名。对 Native Object 使用 `toString()`、`strval($object)`、`(string) $object`、字符串拼接或 `echo` 时,编译器优先使用实际声明的 `toString()`,若不存在则使用 `__toString()`。 -### 15.5 `json_encode()` +与 PHP 一致,声明合法 `__toString()` 的 Native Class 在编译期隐式满足 `Stringable`; +`$native instanceof Stringable` 折叠为 `true`,但这仍不允许把 Native Object 转换或 +传递为 `Stringable` Interface 值。 + +### 15.5 `count()` 与 `Countable` + +当编译器能静态确定 Native Class 实现了 `Countable` 时,`count($nativeObject)` 等价于 +`$nativeObject->count()`,并直接 lowering 为同一个 Native Call。Native Object 不会因此构造 +Zend Object,也不会进入 `php::fn::count()`。 + +仅仅声明一个名为 `count()` 的方法并不足够;Native Class 必须显式 `implements Countable`,且 +实现会经过内部 Interface 签名校验。首版只支持 `count($nativeObject)` 单参数形式;带 `$mode` +的形式不进入这条 Native 特化路径。 + +### 15.6 Nullsafe operator + +Native root 及每一个中间 receiver 都是 Native pointer 时,`?->` 使用专门的短路 +lowering。每一级只执行一次 `nullptr` 判断,方法参数只在 receiver 非空后求值。最终结果 +为 Native Object 时继续返回 nullable typed pointer;最终结果为 PHP 标量或 PHPX value +时,因为 PHP 语义是 `T|null`,只在结果边界装箱为 `php::Var`。 + +Native nullsafe chain 不能在中间切换到 Zend Object 后继续;该混合对象模型链在编译期 +拒绝,用户应拆成两条语句。Native Property Hook 可以作为最终的直接读取; +`isset()/empty()` 不支持 Hook 属性。 + +### 15.7 `json_encode()` Native Object 没有 `zval` 表示,不能作为 `json_encode()` 或其他 PHP/ZendVM 函数的参数。编译器不会为 `json_encode()` 增加特殊 lowering,也不会隐式构造临时 Zend Object 或 DTO;`json_encode($nativeObject)` 在编译期直接报错。 @@ -1088,21 +1166,34 @@ $json = json_encode($nativeObject->toArray()); | 无类型属性 | 不支持,编译期 FatalError | | 直接属性读写 | 支持 | | 普通成员方法 | 支持 | +| Native Object 参数/返回值 | 必须显式声明具体 Native Class;按 pointer value 传递,不复制对象 | +| non-null Native 参数 | `NativeClass $value` 在函数入口统一拒绝 `nullptr`;进入函数体后保证非空 | +| nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 | +| Native 参数/返回值的 `&` | 不支持;编译期 FatalError | +| 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 | +| Native variadic、union/intersection | 不支持;编译期 FatalError | | `__construct()` | 支持 | | `clone` / `__clone()` | 支持 | | Getter/Setter 注解 | 支持 | -| Property Hook | 支持 | +| Property Hook | 支持直接 get/set;间接写入、复合写入、引用、isset/empty 不支持 | | Trait AST 注入 | 支持,注入完成后按普通 Native member 编译 | | `readonly` | 不支持,编译期 FatalError;PHP readonly 是依赖 Zend 属性初始化状态的运行时机制,与 Native 固定裸字段模型不兼容 | | `toArray()`/`toInt()` 等关键词转换 | 支持,要求 Native Class 声明零参数且返回类型完全一致的方法 | | `toString()` / `__toString()` | 支持确定 Native Call;字符串强转、`strval()`、拼接和 `echo` 使用同一规则 | +| `count($nativeObject)` | 支持确定 Native Call;要求 Native Class 实现 `Countable`,首版限单参数形式 | +| `isset()` / `empty()` | 支持裸指针槽及纯 Native 命名属性链,逐级短路,不进入 ZendVM | +| `is_null()` | 支持 Native typed pointer,直接与 `nullptr` 比较 | +| Nullsafe `?->` | 支持纯 Native receiver chain;Native 返回保持 typed pointer,标量返回按 `T|null` 装箱 | | `__invoke()` | 支持确定 Native Call | | `__destruct()` | 支持,由 GC finalization 触发且每个对象最多一次 | | Native Class 单继承 | 支持;与普通 ZendVM class 禁止互相继承 | +| Native abstract class / abstract method | 支持;生成 pure virtual thunk,具体子类在编译期完成实现检查 | | override method | 支持,继承链同名实例方法生成 virtual dispatch thunk | | 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 | | Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 | | `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 | +| `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 | +| `==` / `!=`、大小及算术/位运算 | 不支持,编译期 FatalError;值相等应使用显式 Native 方法 | | 动态属性 | 不支持 | | `$nativeObject->$expr()` | 不支持,只允许命名方法调用 | | `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 | @@ -1110,6 +1201,7 @@ $json = json_encode($nativeObject->toArray()); | `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 | | `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler | | Reflection | 不支持 | +| `get_class()` / `get_parent_class()` / `get_called_class()` | 不支持 Native runtime introspection;使用 `self::class`、`parent::class` 或具体类名 | | WeakReference | 不支持 | | PHP serialize | 不支持 | | PHP `json_encode()` | 不支持直接传入 Native Object;先显式调用 `toArray()` | @@ -1117,10 +1209,14 @@ $json = json_encode($nativeObject->toArray()); | 动态 PHP/eval 使用 | 不支持 | | 普通 PHP array 保存 Native Object | 不支持 | | Box/Std Container 属性 | 不支持 | -| Box/Std Container 保存 Native Object | 不支持 | +| Box 保存 Native Object | 不支持 | +| 局部 Std Container 保存 Native Object | 支持具体 Native class value type;容器 Root Frame 参与 GC tracing | +| 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 清理 | -| 全 non-nullable 构造依赖环 | 不支持,至少需要一条 nullable 边 | +| global/static local 类型 | 第一次 Native 赋值固定 C++ slot 类型;后续可写入其 Native 子类或 null,不可改为基类/无关类 | +| Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 | +| late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 | ## 17. 编译器目录与隔离要求 @@ -1216,7 +1312,7 @@ Native Class 的主要路径必须满足: 4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。 5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。 6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。 -7. 支持 Property Hook 与复合写入 lowering。 +7. 支持 Property Hook 的直接 getter/setter lowering;复合写入等动态语义编译期拒绝。 8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。 9. 最后评估 `json_encode()`、栈分配与逃逸分析。 diff --git a/examples/native/class.php b/examples/native/class.php new file mode 100644 index 00000000..f7b3539e --- /dev/null +++ b/examples/native/class.php @@ -0,0 +1,51 @@ +x, $this->y); + } + + function toBool(): bool + { + return $this->x != 0 || $this->y != 0; + } +} + +function bar(Point $point) +{ + $point->x += 333; + $point->y += 777; +} + +function main() +{ + $p = new Point(); + $p->x = 100; + $p->y = 900; + echo $p, "\n"; + $p->foo(); + + $array = std::array(Point::class, 10); + $array[0] = $p; + + echo $array[0], "\n"; + $array[0]->foo(); + +// bar($array[0]); +// echo $array[0], "\n"; + + bar($p); + echo $p, "\n"; + + $p2 = new Point(); + if ($p2->toBool()) { + echo "p2 is not null\n"; + } +} \ No newline at end of file diff --git a/phpunit/code/native-class-any-escape.php b/phpunit/code/native-class-any-escape.php new file mode 100644 index 00000000..616e969b --- /dev/null +++ b/phpunit/code/native-class-any-escape.php @@ -0,0 +1,12 @@ +__construct(); +} diff --git a/phpunit/code/native-class-first-class-callable.php b/phpunit/code/native-class-first-class-callable.php new file mode 100644 index 00000000..6f00134a --- /dev/null +++ b/phpunit/code/native-class-first-class-callable.php @@ -0,0 +1,13 @@ +run(...); +} diff --git a/phpunit/code/native-class-function-first-class-callable.php b/phpunit/code/native-class-function-first-class-callable.php new file mode 100644 index 00000000..a7e6c5b7 --- /dev/null +++ b/phpunit/code/native-class-function-first-class-callable.php @@ -0,0 +1,11 @@ + $this->stored; + set => $this->stored = $value; + } +} + +function native_property_hook_compound(): void +{ + $object = new NativePropertyHookCompound(); + $object->value += 1; +} diff --git a/phpunit/code/native-class-property-hook-indirect-reference.php b/phpunit/code/native-class-property-hook-indirect-reference.php new file mode 100644 index 00000000..b62e8648 --- /dev/null +++ b/phpunit/code/native-class-property-hook-indirect-reference.php @@ -0,0 +1,18 @@ + $this->stored; + set => $this->stored = $value; + } +} + +function invalidNativePropertyHookIndirectReference(): void +{ + $object = new NativePropertyHookIndirectReference(); + $reference =& $object->items[0]; +} diff --git a/phpunit/code/native-class-property-hook-indirect-unset.php b/phpunit/code/native-class-property-hook-indirect-unset.php new file mode 100644 index 00000000..d4274b97 --- /dev/null +++ b/phpunit/code/native-class-property-hook-indirect-unset.php @@ -0,0 +1,18 @@ + $this->stored; + set => $this->stored = $value; + } +} + +function invalidNativePropertyHookIndirectUnset(): void +{ + $object = new NativePropertyHookIndirectUnset(); + unset($object->items[0]); +} diff --git a/phpunit/code/native-class-property-hook-indirect-write.php b/phpunit/code/native-class-property-hook-indirect-write.php new file mode 100644 index 00000000..8f89d0f5 --- /dev/null +++ b/phpunit/code/native-class-property-hook-indirect-write.php @@ -0,0 +1,18 @@ + $this->stored; + set => $this->stored = $value; + } +} + +function invalidNativePropertyHookIndirectWrite(): void +{ + $object = new NativePropertyHookIndirectWrite(); + $object->items[] = 1; +} diff --git a/phpunit/code/native-class-property-hook-isset.php b/phpunit/code/native-class-property-hook-isset.php new file mode 100644 index 00000000..915e012b --- /dev/null +++ b/phpunit/code/native-class-property-hook-isset.php @@ -0,0 +1,15 @@ + 1; + } +} + +function native_property_hook_isset(): void +{ + $object = new NativePropertyHookIsset(); + isset($object->value); +} diff --git a/phpunit/code/native-class-reference-assignment.php b/phpunit/code/native-class-reference-assignment.php new file mode 100644 index 00000000..2fce2487 --- /dev/null +++ b/phpunit/code/native-class-reference-assignment.php @@ -0,0 +1,10 @@ +toRef(); +} diff --git a/phpunit/code/native-class-reference-parameter.php b/phpunit/code/native-class-reference-parameter.php new file mode 100644 index 00000000..a99b1368 --- /dev/null +++ b/phpunit/code/native-class-reference-parameter.php @@ -0,0 +1,6 @@ +value(second: 50); +} + diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index 7ceea4a8..a29db234 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -6,6 +6,27 @@ use TypePhp\Exception\TestError; final class NativeClassValidationTest extends \BaseTest { + public function testRejectsNativeAttributeOnInterface(): void + { + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage('Native can only be applied to named classes'); + $this->compile('native-class-attribute-interface.php'); + } + + public function testRejectsNativeAttributeOnTrait(): void + { + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage('Native can only be applied to named classes'); + $this->compile('native-class-attribute-trait.php'); + } + + public function testRejectsNativeAttributeOnEnum(): void + { + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage('Native can only be applied to named classes'); + $this->compile('native-class-attribute-enum.php'); + } + public function testRejectsUntypedProperty(): void { $this->expectException(TestError::class); @@ -48,6 +69,55 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-std-container-property.php'); } + public function testRejectsNativeStdContainerConversionToPhpArray(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-escape.php'); + } + + public function testRejectsNativeStdContainerPassedAsPhpValue(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-argument.php'); + } + + public function testRejectsCompoundWritesToNativePropertyHooks(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native property hooks only support direct reads and assignments'); + $this->compile('native-class-property-hook-compound.php'); + } + + public function testRejectsIssetOnNativePropertyHooks(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('isset()/empty() are not supported for Native property hooks'); + $this->compile('native-class-property-hook-isset.php'); + } + + public function testRejectsIndirectWritesToNativePropertyHooks(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native property hooks only support direct reads and assignments'); + $this->compile('native-class-property-hook-indirect-write.php'); + } + + public function testRejectsIndirectUnsetOnNativePropertyHooks(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native property hooks only support direct reads and assignments'); + $this->compile('native-class-property-hook-indirect-unset.php'); + } + + public function testRejectsIndirectReferencesToNativePropertyHooks(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native property hooks only support direct reads and assignments'); + $this->compile('native-class-property-hook-indirect-reference.php'); + } + public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void { $this->expectException(TestError::class); @@ -83,6 +153,20 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-closure-capture.php'); } + public function testRejectsNativeMethodFirstClassCallable(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object methods cannot be converted to Zend closures'); + $this->compile('native-class-first-class-callable.php'); + } + + public function testRejectsNativeAbiFunctionFirstClassCallable(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native ABI functions cannot be converted to Zend closures'); + $this->compile('native-class-function-first-class-callable.php'); + } + public function testRejectsNativeObjectClosureParameter(): void { $this->expectException(TestError::class); @@ -107,10 +191,73 @@ final class NativeClassValidationTest extends \BaseTest public function testRejectsUnsupportedNativeObjectUnion(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native object types cannot be combined with other union or intersection members'); + $this->expectExceptionMessage('Native object types do not support union or intersection declarations; use nullable ?Class syntax'); $this->compile('native-class-union-signature.php'); } + public function testRejectsNativeObjectReferenceParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object parameters cannot be passed by reference'); + $this->compile('native-class-reference-parameter.php'); + } + + public function testRejectsNativeObjectReferenceReturn(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be returned by reference'); + $this->compile('native-class-reference-return.php'); + } + + public function testRejectsNativeObjectReferenceAssignment(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be referenced; object assignment already shares identity'); + $this->compile('native-class-reference-assignment.php'); + } + + public function testRejectsNativeObjectReferenceKeywordMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be referenced; object assignment already shares identity'); + $this->compile('native-class-reference-method.php'); + } + + public function testRejectsNativeObjectReferenceFunction(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be referenced; object assignment already shares identity'); + $this->compile('native-class-reference-function.php'); + } + + public function testRejectsNativeObjectVariadicParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object parameters cannot be variadic'); + $this->compile('native-class-variadic-parameter.php'); + } + + public function testRejectsNativeObjectPassedToUntypedParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot cross a PHP/ZendVM argument boundary'); + $this->compile('native-class-untyped-parameter.php'); + } + + public function testRejectsNativeObjectNullUnionInFavorOfNullableSyntax(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object types do not support union or intersection declarations; use nullable ?Class syntax'); + $this->compile('native-class-null-union-parameter.php'); + } + + public function testRejectsImplicitNullableNativeParameterDefault(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('A Native object parameter with a null default must use explicit nullable ?Class syntax'); + $this->compile('native-class-implicit-nullable-parameter.php'); + } + public function testRejectsIncorrectNativeKeywordReturnType(): void { $this->expectException(TestError::class); @@ -243,4 +390,138 @@ final class NativeClassValidationTest extends \BaseTest $this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`'); $this->compile('native-class-internal-interface-parameter.php'); } + + public function testRejectsExplicitNativeConstructorCall(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Explicit calls to native object constructors are not supported'); + $this->compile('native-class-explicit-constructor-call.php'); + } + + public function testRejectsInternalInterfaceThatPhpClassesCannotImplement(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('cannot implement internal interface `Throwable`'); + $this->compile('native-class-non-implementable-interface.php'); + } + + public function testCountRequiresCountableContract(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('count() requires a native class implementing Countable'); + $this->compile('native-class-count-without-countable.php'); + } + + public function testRejectsNativeLooseEquality(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support the `==` operator; use `===` or `!==` for identity comparison'); + $this->compile('native-class-loose-equality.php'); + } + + public function testRejectsNativeArithmeticOperators(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects do not support the `+` operator'); + $this->compile('native-class-arithmetic-operator.php'); + } + + public function testRejectsInaccessibleNativeCloneMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Call to private NativePrivateClone::__clone()'); + $this->compile('native-class-private-clone.php'); + } + + public function testRejectsNativeObjectReferenceParameterBeforeVirtualAbiGeneration(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object parameters cannot be passed by reference'); + $this->compile('native-class-virtual-byref-variance.php'); + } + + public function testRejectsNamedArgumentHoleInNativeVirtualCall(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Named calls to Native virtual methods cannot skip an earlier optional parameter'); + $this->compile('native-class-virtual-named-gap.php'); + } + + public function testRejectsLateStaticConstructionInNativeClass(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support `new static()`'); + $this->compile('native-class-new-static.php'); + } + + public function testRejectsGetCalledClassInNativeClass(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support late static binding'); + $this->compile('native-class-get-called-class.php'); + } + + public function testRejectsGetClassForNativeObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support runtime class introspection'); + $this->compile('native-class-get-class.php'); + } + + public function testRejectsImplicitGetClassInNativeMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support runtime class introspection'); + $this->compile('native-class-get-class-implicit.php'); + } + + public function testRejectsGetParentClassForNativeObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support runtime class introspection'); + $this->compile('native-class-get-parent-class.php'); + } + + public function testRejectsChangingAnInferredNativeGlobalType(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native global/static slot cannot change from `NativeGlobalFirst` to `NativeGlobalSecond`'); + $this->compile('native-class-global-type-change.php'); + } + + public function testRejectsLateStaticConstantResolutionInNativeClass(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support late static binding; use `self::` or a concrete class name'); + $this->compile('native-class-late-static-constant.php'); + } + + public function testRejectsNativeObjectCastToZendObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be converted to Zend objects'); + $this->compile('native-class-object-cast.php'); + } + + public function testRejectsErasingNativeObjectTypeWithAny(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be converted to mixed with any()'); + $this->compile('native-class-any-escape.php'); + } + + public function testRejectsBareReturnForNullableNativeObjectType(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('A function with a Native object return type must return a value'); + $this->compile('native-class-bare-return.php'); + } + + public function testRejectsNullForNonNullableNativeObjectReturn(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('The return type is non-nullable native object `NativeNullReturnValue`'); + $this->compile('native-class-null-return.php'); + } + } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 6113c7ab..b7171895 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -708,6 +708,9 @@ class CompilerBase implements PropertyAccessContext if ($expr->getLine() === $this->debugLine) { dump($expr); } + if ($expr instanceof Node\Expr\BinaryOp) { + $this->assertNativeObjectBinaryOperatorSupported($expr); + } switch ($type) { case 'Expr_Isset': return $this->parseIsset($expr); @@ -1879,9 +1882,31 @@ class CompilerBase implements PropertyAccessContext protected function detectClassOfExpr(NodeAbstract $expr): string { + // Error suppression changes diagnostics only; it must never erase the + // static type of the wrapped expression. This is especially important + // for Native objects because treating their typed pointer as php::Var + // would cross the ZendVM boundary. + if ($expr instanceof Expr\ErrorSuppress) { + return $this->detectClassOfExpr($expr->expr); + } if ($expr instanceof Expr\Clone_) { return $this->detectClassOfExpr($expr->expr); } + if ($expr instanceof Expr\NullsafeMethodCall) { + return $this->detectClassOfExpr(new Expr\MethodCall( + $expr->var, + $expr->name, + $expr->args, + $expr->getAttributes(), + )); + } + if ($expr instanceof Expr\NullsafePropertyFetch) { + return $this->detectClassOfExpr(new Expr\PropertyFetch( + $expr->var, + $expr->name, + $expr->getAttributes(), + )); + } if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { return 'Closure'; } @@ -1921,6 +1946,9 @@ class CompilerBase implements PropertyAccessContext return $this->getFullClassName(); } if ($class === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError($expr, 'Native classes do not support `new static()`'); + } // 无法在编译期获得 static 类的准确类名 return ''; } else { @@ -2225,6 +2253,14 @@ class CompilerBase implements PropertyAccessContext return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';'; } if ($v->expr === null) { + if (!$this->context->inClosure + && $this->getNativeObjectReturnType($this->functionDef) !== null + ) { + $this->fatalError( + $v, + 'A function with a Native object return type must return a value', + ); + } $nullExpr = new Expr\ConstFetch(new Node\Name('null')); if ($this->shouldCheckClosureReturnType()) { $this->checkCompositeTypeAssignment( @@ -2348,7 +2384,31 @@ class CompilerBase implements PropertyAccessContext "The return type is native object `{$nativeReturnClass}`, `{$objectClass}` given" ); } - return 'return ' . $this->parseExprAsValue($v->expr) . ';'; + $afterStmtCount = count($this->context->afterStmtLines); + $returnExpr = $this->parseExprAsValue($v->expr); + $returnCode = $this->functionDef->returnNullable + ? $returnExpr + : 'php::nativeRequireObject(' . $returnExpr . ', "' + . addslashes($nativeReturnClass) . '")'; + + if (count($this->context->afterStmtLines) === $afterStmtCount) { + return 'return ' . $returnCode . ';'; + } + + // Cleanup emitted by the expression (for example restoring @'s + // error_reporting state) must run before either the non-null + // return check or the actual return. Materialize the pointer in a + // precise root slot, then append the boundary operation after the + // expression's cleanup statements. + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeReturnClass)); + $this->addNativeObject($tmpVar, $nativeReturnClass); + $finalReturn = $this->functionDef->returnNullable + ? $tmpVar + : 'php::nativeRequireObject(' . $tmpVar . ', "' + . addslashes($nativeReturnClass) . '")'; + $this->context->afterStmtLines[] = $this->getIndent() . 'return ' . $finalReturn . ';'; + return $tmpVar . ' = ' . $returnExpr . ';'; } $expr = $this->parseExprAsValue($v->expr); $returnType = $this->getReturnType(); @@ -2746,6 +2806,9 @@ class CompilerBase implements PropertyAccessContext protected function detectTypeOfExpr($expr): string { + if ($expr instanceof Expr\ErrorSuppress) { + return $this->detectTypeOfExpr($expr->expr); + } if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) { $keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name)); if ($keywordType !== null) { @@ -3071,6 +3134,12 @@ class CompilerBase implements PropertyAccessContext } $target = $this->preparePropertyWriteTarget($var); + if ($this->isNativeObjectPropertyHook($var)) { + $this->fatalError( + $var, + 'Native property hooks only support direct reads and assignments', + ); + } $getter = $this->getPropertyHookGetter($var); $setter = $this->getPropertyHookSetter($var); if ($getter !== null && $setter === null) { @@ -3111,6 +3180,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreInc(Expr\PreInc $expr): string { $this->assertNotNullsafeWriteContext($expr->var); + $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, '+', true); if ($result !== null) { return $result; @@ -3497,6 +3567,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string { $this->assertNotNullsafeWriteContext($expr->var); + $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, $op, false); if ($result !== null) { return $result; @@ -3545,6 +3616,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreDec(Expr\PreDec $expr): string { $this->assertNotNullsafeWriteContext($expr->var); + $this->assertNativePropertyHookDirectWriteTarget($expr->var); $result = $this->genDynamicPropIncDec($expr->var, '-', true); if ($result !== null) { return $result; @@ -3631,6 +3703,9 @@ class CompilerBase implements PropertyAccessContext $className = $this->parseIdentifier($expr->class); if ($this->isNameExpr($expr->class)) { if ($className === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError($expr, 'Native classes do not support `new static()`'); + } $cePtr = Symbol::getCalledCe(); } else { if ($className === 'self') { @@ -3664,7 +3739,8 @@ class CompilerBase implements PropertyAccessContext $this->fatalError($expr, "Native class `{$className}` does not have a constructor"); } return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor - . ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); })'; + . ', [&](auto &this_) { ' + . $this->getNativeObjectInitializerName($className) . '(this_); })'; } $nativeCtor = $this->getNativeMethod($expr, $className, '__construct'); if ($nativeCtor === false) { @@ -3674,7 +3750,8 @@ class CompilerBase implements PropertyAccessContext ? '' : ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor); return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor - . ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); ' + . ', [&](auto &this_) { ' + . $this->getNativeObjectInitializerName($className) . '(this_); ' . self::PREFIX . $nativeCtor . '(this_' . $args . '); })'; } $cePtr = $this->getClassEntryPtr($className); @@ -3696,18 +3773,34 @@ class CompilerBase implements PropertyAccessContext $this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand'); $class = $this->detectClassOfExpr($expr->expr); if ($this->isNativeObjectClass($class)) { - if (!$this->isVarExpr($expr->expr)) { - $this->fatalError($expr, 'Native object clone currently requires a typed variable'); - } - $source = $this->parseIdentifier($expr->expr); + $source = $this->isVarExpr($expr->expr) + ? $this->parseIdentifier($expr->expr) + : $this->materializeNativeObjectReceiver($expr->expr, $class); $cpp = $this->getNativeObjectCppName($class); $descriptor = $this->getNativeObjectDescriptorName($class); - $classDef = $this->getClass($class); $initializer = ''; - if ($classDef->hasMethod('__clone')) { - $clone = self::PREFIX . $this->getNativeName('__clone', $classDef->namespace, $classDef->name); + $cloneMethod = $this->findNativeObjectMethod($class, '__clone'); + if ($cloneMethod !== null) { + $declaringClassName = $cloneMethod->functionDef->declaringClass; + $declaringClass = $this->getClass($declaringClassName); + if (!$this->checkAccessible($declaringClass, $cloneMethod->flags)) { + $visibility = $this->visibilityLabel($cloneMethod->flags); + $this->fatalError( + $expr, + "Call to {$visibility} {$declaringClassName}::__clone()", + ); + } + $clone = self::PREFIX . $this->getNativeName( + '__clone', + $declaringClass->namespace, + $declaringClass->name, + ); $initializer = $clone . '(this_); '; } + if ($this->nativeObjectUsesVirtualClone($class)) { + return $this->getNativeObjectReceiver($source) . '.' + . self::NATIVE_VIRTUAL_CLONE_METHOD . '()'; + } return 'php::nativeClone<' . $cpp . '>(' . $descriptor . ', ' . $this->getNativeObjectReceiver($source) . ', [&](auto &this_) { ' . $initializer . '})'; @@ -3746,7 +3839,13 @@ class CompilerBase implements PropertyAccessContext // a non-variable left operand. Native objects cannot cross into a // Variant, so sequence the original pointer expression directly. $value = $this->parseExprAsValue($expr->expr); - return '(static_cast(' . $value . '), ' . ($result ? 'true' : 'false') . ')'; + if ($result) { + // Static assignability proves the class relationship, but a + // nullable Native slot still follows PHP: null instanceof T + // is false. The raw-pointer null check is the only runtime work. + return '(' . $value . ' != nullptr)'; + } + return '(static_cast(' . $value . '), false)'; } if ($this->isNameExpr($expr->class)) { @@ -3875,7 +3974,7 @@ class CompilerBase implements PropertyAccessContext if ($var->default) { $class = $this->detectClassOfExpr($var->default); if ($this->isNativeObjectClass($class)) { - $this->promoteGlobalOrStaticToNativeObject($varName, $class); + $this->promoteGlobalOrStaticToNativeObject($varName, $class, $var->default); } } @@ -4042,11 +4141,28 @@ class CompilerBase implements PropertyAccessContext protected function parseChainedExpr(NodeAbstract $node, string $op, bool $getValue = false): string { + if ($op === self::OP_REFVAL) { + $this->assertNativeObjectReferenceForbidden($node, $node); + } + if (in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) { + $nativePresence = $this->parseNativeObjectPresenceChain($node, $op); + if ($nativePresence !== null) { + return $nativePresence; + } + } // TypePHP 编译器不允许操作未定义的变量,PHP 的 isset($var) 可能 $var 未定义 $this->checkVarMustExist($node, $this->parseIdentifier($node)); $fn = $this->getChainedFunc($op); $expr = $node; if ($this->isVarExpr($expr)) { + $nativeObject = $this->parseIdentifier($expr); + if ($this->isNativeObjectVar($nativeObject)) { + return match ($op) { + self::OP_ISSET, self::OP_NOT_EMPTY => '(' . $nativeObject . ' != nullptr)', + self::OP_EMPTY => '(' . $nativeObject . ' == nullptr)', + default => $fn . '(' . $this->parseExpr($expr) . ')', + }; + } if (!$getValue) { return $fn . '(' . $this->parseExpr($expr) . ')'; } @@ -4060,6 +4176,13 @@ class CompilerBase implements PropertyAccessContext if ($op === self::OP_REFVAL) { return $prop . '.toReference()'; } + if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) { + return match ($op) { + self::OP_ISSET, self::OP_NOT_EMPTY => '(' . $prop . ' != nullptr)', + self::OP_EMPTY => '(' . $prop . ' == nullptr)', + default => $fn . '(' . $prop . ')', + }; + } return $fn . '(' . $prop . ')'; } } @@ -4120,9 +4243,93 @@ class CompilerBase implements PropertyAccessContext } } + /** + * Lower a named Native property chain without converting its raw pointers + * to Variant. The short-circuit lambda preserves PHP's isset()/empty() + * behavior when either the root or an intermediate Native slot is null. + */ + protected function parseNativeObjectPresenceChain(NodeAbstract $node, string $op): ?string + { + $properties = []; + $base = $node; + while ($base instanceof Expr\PropertyFetch) { + if (!$base->name instanceof Node\Identifier) { + return null; + } + $properties[] = $base; + $base = $base->var; + } + if ($properties === [] || !$this->isVarExpr($base)) { + return null; + } + + $baseName = $this->parseIdentifier($base); + if (!$this->isNativeObjectVar($baseName)) { + return null; + } + $this->checkVarMustExist($base, $baseName); + + $properties = array_reverse($properties); + $nullResult = $op === self::OP_EMPTY ? 'true' : 'false'; + $current = $this->genTmpVarName(); + $class = $this->getNativeObjectVarClass($baseName); + $code = '[&]() -> bool {' . PHP_EOL; + $code .= $this->getIndent() . 'auto *' . $current . ' = ' . $baseName . ';' . PHP_EOL; + $code .= $this->getIndent() . 'if (' . $current . ' == nullptr) { return ' . $nullResult . '; }' . PHP_EOL; + + $last = array_key_last($properties); + foreach ($properties as $index => $propertyExpr) { + $property = $propertyExpr->name->toString(); + $resolution = $this->resolveNativeInstanceProperty($propertyExpr, $property, $class); + if ($resolution === null) { + $this->fatalError($propertyExpr, "Native class `{$class}` has no property `\${$property}`"); + } + $this->applyNativePropertyAccessResult($propertyExpr, $resolution); + $definition = $resolution->propertyDef; + if ($definition->getter !== null || $definition->setter !== null) { + $this->fatalError( + $propertyExpr, + 'isset()/empty() are not supported for Native property hooks', + ); + } + $field = $current . '->' + . $this->getNativeObjectPropertyCppName($definition, $resolution->classDef); + $isNativePointer = $definition->type === Type::OBJECT + && $this->isNativeObjectClass($definition->class); + + if ($index !== $last) { + if (!$isNativePointer) { + $this->fatalError( + $propertyExpr, + 'Native isset()/empty() chains may only traverse Native object properties', + ); + } + $next = $this->genTmpVarName(); + $code .= $this->getIndent() . 'auto *' . $next . ' = ' . $field . ';' . PHP_EOL; + $code .= $this->getIndent() . 'if (' . $next . ' == nullptr) { return ' + . $nullResult . '; }' . PHP_EOL; + $current = $next; + $class = $definition->class; + continue; + } + + if ($isNativePointer) { + $condition = '(' . $field . ($op === self::OP_EMPTY ? ' == ' : ' != ') . 'nullptr)'; + } else { + $condition = $this->getChainedFunc($op) . '(' . $field . ')'; + } + $code .= $this->getIndent() . 'return ' . $condition . ';' . PHP_EOL; + } + return $code . $this->getIndent() . '}()'; + } + protected function parseCastArray(Expr\Cast\Array_ $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); + $native = $this->parseNativeObjectExplicitConversion($expr->expr, 'toArray'); + if ($native !== null) { + return $native; + } return $this->convertArrayExpr($this->parseExprAsValue($expr->expr)); } @@ -4144,6 +4351,10 @@ class CompilerBase implements PropertyAccessContext protected function parseCastDouble(mixed $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); + $native = $this->parseNativeObjectExplicitConversion($expr->expr, 'toFloat'); + if ($native !== null) { + return $native; + } return $this->convertFloatExpr( $this->parseIdentifier($expr->expr), $this->detectTypeOfExpr($expr->expr) @@ -4581,9 +4792,11 @@ class CompilerBase implements PropertyAccessContext if (isset($this->context->globalVars[$name])) { continue; } + $stdContainerInfo = null; $code .= $this->getIndent(); if ($type === Type::STD_ARRAY) { $info = $this->context->stdArrays[$name]; + $stdContainerInfo = $info; if (isset($info['boxExpr'])) { $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; } else { @@ -4596,6 +4809,7 @@ class CompilerBase implements PropertyAccessContext } } elseif ($type === Type::STD_VECTOR) { $info = $this->context->stdContainers[$name]; + $stdContainerInfo = $info; if (isset($info['boxExpr'])) { $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; } else { @@ -4614,6 +4828,7 @@ class CompilerBase implements PropertyAccessContext } } elseif ($type === Type::STD_MAP || $type === Type::STD_ORDERED_MAP) { $info = $this->context->stdContainers[$name]; + $stdContainerInfo = $info; if (isset($info['boxExpr'])) { $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; } else { @@ -4633,6 +4848,14 @@ class CompilerBase implements PropertyAccessContext $code .= ';'; } $code .= PHP_EOL; + if ($stdContainerInfo !== null + && isset($stdContainerInfo['class']) + && $this->isNativeObjectClass($stdContainerInfo['class']) + ) { + $rootType = 'std::remove_reference_t'; + $code .= $this->getIndent() . 'php::NativeContainerRootFrame<' . $rootType . '> ' + . $name . '_native_root_frame(' . $name . '_ref);' . PHP_EOL; + } } return $code; } @@ -4717,7 +4940,10 @@ class CompilerBase implements PropertyAccessContext return ''; } if ($this->getNativeObjectReturnType($this->functionDef) !== null) { - return $this->getIndent() . 'return nullptr;'; + $class = $this->getReturnClass(); + $pointerType = $this->getNativeObjectPointerType($class); + return $this->getIndent() . 'return static_cast<' . $pointerType . '>(' + . 'php::nativeGcRequireObject(nullptr, "' . addslashes($class) . '"));'; } if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) { return $this->genUnionCheckedReturn(self::VALUE_NULL); diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 014dd067..7d7bd719 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -9,6 +9,7 @@ namespace TypePhp\Context; use PhpParser\Node\Expr\Variable; +use PhpParser\NodeAbstract; use TypePhp\Entity\ClassDef; use TypePhp\Entity\FunctionDef; use TypePhp\Entity\InterfaceDef; @@ -74,7 +75,11 @@ trait CompilationStateTrait $this->globalVars[$name] = $type; } - protected function promoteGlobalOrStaticToNativeObject(string $name, string $class): void + protected function promoteGlobalOrStaticToNativeObject( + string $name, + string $class, + ?NodeAbstract $node = null, + ): void { $class = ltrim($class, '\\'); if ($this->hasStaticVar($name)) { @@ -86,6 +91,19 @@ trait CompilationStateTrait } else { return; } + if (isset($this->nativeGlobalObjects[$slot])) { + $existing = $this->nativeGlobalObjects[$slot]; + if (!$this->isObjectClassStaticallyAssignableTo($class, $existing)) { + $message = "Native global/static slot cannot change from `{$existing}` to `{$class}`"; + if ($node !== null) { + $this->fatalError($node, $message); + } + $this->error($message); + } + // The first assignment fixes the C++ slot type. A derived object + // remains assignable, but must not narrow later uses of the slot. + $class = $existing; + } $this->globalVars[$slot] = $this->getNativeObjectPointerType($class); $this->nativeGlobalObjects[$slot] = $class; $this->addNativeObject($name, $class); diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index c76c2e9d..767c6018 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -32,6 +32,16 @@ class FunctionContext /** @var array Native Object pointer variable => fully-qualified class name. */ public array $nativeObjects = []; + /** + * Native pointer variables proven non-null at the current parse point. + * + * Non-null Native parameters enter this set after their single function + * entry check. Any assignment or unset conservatively removes the proof. + * + * @var array + */ + public array $nonNullNativeObjects = []; + /** * Declared object constraints that are not used for native-call dispatch. * @@ -94,6 +104,7 @@ class FunctionContext $this->arguments = []; $this->objects = []; $this->nativeObjects = []; + $this->nonNullNativeObjects = []; $this->declaredObjects = []; $this->stdArrays = []; $this->stdContainers = []; @@ -133,12 +144,14 @@ class FunctionContext int $tmpVarIndex, array $declaredObjects, array $nativeObjects = [], + array $nonNullNativeObjects = [], ): void { $this->localVars = $localVars; $this->tmpVarIndex = $tmpVarIndex; $this->declaredObjects = $declaredObjects; $this->nativeObjects = $nativeObjects; + $this->nonNullNativeObjects = $nonNullNativeObjects; $this->beforeStmtLines = []; $this->afterStmtLines = []; $this->objectProps = []; diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index c0ac11da..8db7cb0d 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -24,6 +24,8 @@ class FunctionDef public string $params = ''; public string $namespace; public bool $method = false; + /** Abstract method contract; it has metadata/default helpers but no C++ body symbol. */ + public bool $abstractMethod = false; /** Fully-qualified declaring class for methods; empty for free functions. */ public string $declaringClass = ''; public bool $stub = false; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 472d365d..7fdc07d8 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -20,7 +20,12 @@ use TypePhp\Generator\Symbol; trait CallArgumentGenerator { - protected function parseNativeCallArgs(array $callArgs, string $nativeFunc, int $parameterOffset = 0): string + protected function parseNativeCallArgs( + array $callArgs, + string $nativeFunc, + int $parameterOffset = 0, + bool $deferTrailingDefaults = false, + ): string { $functionDef = $this->getFunction($nativeFunc); $providedArgs = []; @@ -65,6 +70,12 @@ trait CallArgumentGenerator // lowered in source order; sorting raw AST arguments here would also // reorder their side effects. if ($hasNamedArg) { + $lastProvidedIndex = $providedArgs === [] + ? $parameterOffset - 1 + : max(array_keys($providedArgs)); + if ($deferTrailingDefaults && $variadicArgCount > 0) { + $lastProvidedIndex = $variadicArgIndex; + } // 命名参数中间存在空洞,需要使用默认参数填充 foreach ($functionDef->argInfoList as $k => $argInfo) { if ($k < $parameterOffset) { @@ -74,6 +85,20 @@ trait CallArgumentGenerator continue; } if (!isset($providedArgs[$k])) { + // A Native virtual overload must omit a trailing default + // so the dynamically selected implementation supplies it. + // A named-argument hole before a later argument cannot be + // represented by a positional C++ overload without a + // presence mask, so reject that uncommon shape explicitly. + if ($deferTrailingDefaults && $k > $lastProvidedIndex) { + continue; + } + if ($deferTrailingDefaults) { + $this->fatalError( + reset($callArgs), + 'Named calls to Native virtual methods cannot skip an earlier optional parameter', + ); + } if ($argInfo->default === '') { $errorNode = null; foreach ($callArgs as $a) { @@ -97,7 +122,7 @@ trait CallArgumentGenerator if (count($sourceArgs) === 0 and count($functionDef->argInfoList) === $parameterOffset + 1 and $functionDef->argInfoList[$parameterOffset]->variadic) { - return '{}'; + return $deferTrailingDefaults ? '' : '{}'; } $resolvedArgs = []; @@ -606,6 +631,12 @@ trait CallArgumentGenerator protected function parseCallArgValue(Node\Arg $arg): string { $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); + if ($this->isVarExpr($arg->value)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $arg, + $this->parseIdentifier($arg->value), + ); + } $class = $this->detectClassOfExpr($arg->value); if ($class !== '' && $this->isNativeObjectClass($class)) { $this->fatalError( @@ -656,6 +687,8 @@ trait CallArgumentGenerator $arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg); } + $this->assertNativeObjectReferenceForbidden($arg->value, $arg); + if ($this->isVarExpr($arg->value)) { return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value)); } diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index f4f89def..ab31e892 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -21,6 +21,8 @@ use PhpParser\Node; trait NativeClassSupportTrait { + private const string NATIVE_VIRTUAL_CLONE_METHOD = '__typephp_native_clone'; + /** * Magic methods whose semantics require Zend object handlers, runtime * method resolution, dynamic properties, or Zend serialization state. @@ -41,6 +43,15 @@ trait NativeClassSupportTrait '__debuginfo' => true, ]; + /** Internal interfaces which PHP does not allow an ordinary class to implement directly. */ + private const NON_IMPLEMENTABLE_INTERNAL_INTERFACES = [ + 'throwable' => true, + 'traversable' => true, + 'datetimeinterface' => true, + 'unitenum' => true, + 'backedenum' => true, + ]; + protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void { if (!$this->classDef?->nativeObject @@ -65,6 +76,12 @@ trait NativeClassSupportTrait ClassDef $classDef, string $interfaceName, ): void { + if (isset(self::NON_IMPLEMENTABLE_INTERNAL_INTERFACES[strtolower(ltrim($interfaceName, '\\'))])) { + $this->fatalError( + $node, + "Native class `{$classDef->getNamespacedName(false)}` cannot implement internal interface `{$interfaceName}`", + ); + } $interface = Reflection::getClass($interfaceName); if ($interface === null) { $this->fatalError($node, "Internal interface `{$interfaceName}` is not available"); @@ -244,6 +261,60 @@ trait NativeClassSupportTrait return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; } + /** + * Native objects have identity but no Zend object handler capable of PHP's + * recursive loose/value comparison. Reject unsupported operators before a + * raw pointer can reach php::equals() or a C++ arithmetic expression. + */ + protected function assertNativeObjectBinaryOperatorSupported(Node\Expr\BinaryOp $expr): void + { + $leftNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->left)); + $rightNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->right)); + if (!$leftNative && !$rightNative) { + return; + } + + if ($expr instanceof Node\Expr\BinaryOp\Identical + || $expr instanceof Node\Expr\BinaryOp\NotIdentical + || $expr instanceof Node\Expr\BinaryOp\Coalesce + || $expr instanceof Node\Expr\BinaryOp\Concat + || $expr instanceof Node\Expr\BinaryOp\BooleanAnd + || $expr instanceof Node\Expr\BinaryOp\LogicalAnd + || $expr instanceof Node\Expr\BinaryOp\BooleanOr + || $expr instanceof Node\Expr\BinaryOp\LogicalOr + || $expr instanceof Node\Expr\BinaryOp\LogicalXor + || $expr instanceof Node\Expr\BinaryOp\Pipe + ) { + return; + } + + $operator = match (true) { + $expr instanceof Node\Expr\BinaryOp\Equal => '==', + $expr instanceof Node\Expr\BinaryOp\NotEqual => '!=', + $expr instanceof Node\Expr\BinaryOp\Plus => '+', + $expr instanceof Node\Expr\BinaryOp\Minus => '-', + $expr instanceof Node\Expr\BinaryOp\Mul => '*', + $expr instanceof Node\Expr\BinaryOp\Div => '/', + $expr instanceof Node\Expr\BinaryOp\Mod => '%', + $expr instanceof Node\Expr\BinaryOp\Pow => '**', + $expr instanceof Node\Expr\BinaryOp\Smaller => '<', + $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual => '<=', + $expr instanceof Node\Expr\BinaryOp\Greater => '>', + $expr instanceof Node\Expr\BinaryOp\GreaterOrEqual => '>=', + $expr instanceof Node\Expr\BinaryOp\Spaceship => '<=>', + $expr instanceof Node\Expr\BinaryOp\ShiftLeft => '<<', + $expr instanceof Node\Expr\BinaryOp\ShiftRight => '>>', + $expr instanceof Node\Expr\BinaryOp\BitwiseAnd => '&', + $expr instanceof Node\Expr\BinaryOp\BitwiseOr => '|', + $expr instanceof Node\Expr\BinaryOp\BitwiseXor => '^', + default => $expr->getType(), + }; + $suffix = in_array($operator, ['==', '!='], true) + ? '; use `===` or `!==` for identity comparison' + : ''; + $this->fatalError($expr, "Native objects do not support the `{$operator}` operator{$suffix}"); + } + protected function getNativeObjectCppName(string|ClassDef $class): string { $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); @@ -260,33 +331,76 @@ trait NativeClassSupportTrait return $this->getNativeObjectCppName($class) . ' *'; } + /** + * A statically typed base pointer may hold any Native subclass. Only such + * inheritance hierarchies need a vtable entry for clone; standalone/final + * object layouts retain the zero-overhead static clone path. + */ + protected function nativeObjectUsesVirtualClone(string|ClassDef $class): bool + { + $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); + if ($classDef->extends !== '' && $this->isNativeObjectClass($classDef->extends)) { + return true; + } + $className = $classDef->getNamespacedName(false); + foreach ($this->symbols->classes() as $candidate) { + if (!$candidate->nativeObject + || $this->isSameClassName($candidate->getNamespacedName(false), $className) + ) { + continue; + } + $parent = $candidate->extends; + while ($parent !== '' && $this->isNativeObjectClass($parent)) { + if ($this->isSameClassName($parent, $className)) { + return true; + } + $parent = $this->getClass($parent)->extends; + } + } + return false; + } + protected function getNativeObjectArgumentType(ArgInfo $argument): ?string { $class = $argument->declaredClass ?: $argument->class; - if ((!$argument->byRef && $argument->type !== Type::OBJECT) - || !$this->isNativeObjectClass($class) - ) { + if ($argument->type !== Type::OBJECT || !$this->isNativeObjectClass($class)) { return null; } - return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : ''); + return $this->getNativeObjectPointerType($class); } - protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array - { - $inner = null; - if ($type instanceof Node\NullableType) { - $inner = $type->type; - } elseif ($type instanceof Node\UnionType && count($type->types) === 2) { - foreach ($type->types as $member) { - if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') { - continue; - } - if ($inner !== null) { - return null; - } - $inner = $member; + /** + * A virtual adapter may widen value parameters, but C++ cannot safely + * adapt T*& to Base*&: the callee could replace it with a Base that is not + * a T. Native objects intentionally carry no runtime class tag, so reject + * that one PHP variance case at the declaration boundary. + */ + protected function assertNativeVirtualByRefStorageCompatible( + NodeAbstract $node, + FunctionDef $child, + FunctionDef $parent, + ): void { + foreach ($parent->argInfoList as $index => $parentArgument) { + if (!$parentArgument->byRef || !isset($child->argInfoList[$index])) { + continue; + } + $childArgument = $child->argInfoList[$index]; + $parentStorage = $this->getNativeObjectArgumentType($parentArgument) + ?? $parentArgument->type; + $childStorage = $this->getNativeObjectArgumentType($childArgument) + ?? $childArgument->type; + if ($parentStorage !== $childStorage) { + $this->fatalError( + $node, + 'Native virtual by-reference parameters must keep the same storage type', + ); } } + } + + protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array + { + $inner = $type instanceof Node\NullableType ? $type->type : null; if (!$inner instanceof Node\Name) { return null; } @@ -365,10 +479,62 @@ trait NativeClassSupportTrait return; } $nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind); - if ($nativeClasses !== [] && $this->resolveNullableNativeObjectType($type, $declarationKind) === null) { + if ($nativeClasses !== []) { + $this->fatalError( + $errorNode, + 'Native object types do not support union or intersection declarations; use nullable ?Class syntax', + ); + } + } + + protected function assertNativeObjectFunctionSignature( + Node\Stmt\Function_|Node\Stmt\ClassMethod $node, + FunctionDef $function, + ): void { + if ($this->isNativeObjectClass($function->returnClass) && $function->returnsByRef) { + $this->fatalError($node, 'Native objects cannot be returned by reference'); + } + foreach ($function->argInfoList as $index => $argument) { + $class = $argument->declaredClass ?: $argument->class; + if (!$this->isNativeObjectClass($class)) { + continue; + } + $parameter = $node->params[$index] ?? $node; + if ($argument->byRef) { + $this->fatalError($parameter, 'Native object parameters cannot be passed by reference'); + } + if ($argument->variadic) { + $this->fatalError($parameter, 'Native object parameters cannot be variadic'); + } + if ($parameter instanceof Node\Param + && $parameter->default !== null + && $this->isNull($parameter->default) + && !$argument->nullable + ) { + $this->fatalError( + $parameter, + 'A Native object parameter with a null default must use explicit nullable ?Class syntax', + ); + } + } + } + + /** + * 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. + */ + protected function assertNativeObjectReferenceForbidden( + NodeAbstract $expr, + NodeAbstract $errorNode, + ): void { + $class = $this->detectDeclaredClassOfExpr($expr); + if ($this->isNativeObjectClass($class)) { $this->fatalError( $errorNode, - 'Native object types cannot be combined with other union or intersection members', + 'Native objects cannot be referenced; object assignment already shares identity', ); } } @@ -433,11 +599,29 @@ trait NativeClassSupportTrait return $this->context->nativeObjects[$name] ?? ''; } + protected function markNativeObjectNonNull(string $name): void + { + $this->context->nonNullNativeObjects[$name] = true; + } + + protected function forgetNativeObjectNonNull(string $name): void + { + unset($this->context->nonNullNativeObjects[$name]); + } + + protected function isNativeObjectKnownNonNull(string $name): bool + { + return isset($this->context->nonNullNativeObjects[$name]); + } + protected function getNativeObjectReceiver(string $name): string { if ($name === 'this_') { return 'this_'; } + if ($this->isNativeObjectKnownNonNull($name)) { + return '(*' . $name . ')'; + } $class = $this->getNativeObjectVarClass($name); return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; } @@ -467,6 +651,132 @@ trait NativeClassSupportTrait return $object; } + /** + * Lower a nullsafe chain whose root and every intermediate receiver are + * Native pointers. The generic implementation deliberately uses + * php::Object/Variant and therefore cannot represent this object model. + */ + protected function parseNativeNullsafeAccess( + Node\Expr\PropertyFetch|Node\Expr\MethodCall|Node\Expr\NullsafePropertyFetch|Node\Expr\NullsafeMethodCall $expr, + ): ?string { + $steps = []; + $base = $expr; + while ($base instanceof Node\Expr\PropertyFetch + || $base instanceof Node\Expr\MethodCall + || $base instanceof Node\Expr\NullsafePropertyFetch + || $base instanceof Node\Expr\NullsafeMethodCall + ) { + array_unshift($steps, $base); + $base = $base->var; + } + + $baseClass = $this->detectClassOfExpr($base); + if ($baseClass === '' && $this->isVarExpr($base)) { + $baseName = $this->parseIdentifier($base); + if ($this->isNativeObjectVar($baseName)) { + $baseClass = $this->getNativeObjectVarClass($baseName); + } + } + if (!$this->isNativeObjectClass($baseClass)) { + return null; + } + + $current = $this->isVarExpr($base) + ? $this->parseIdentifier($base) + : $this->materializeNativeObjectReceiver($base, $baseClass); + if ($this->isVarExpr($base) && !$this->hasVar($current) && $current !== 'this_') { + $this->errorUndefinedVariable($base); + } + + $body = ''; + $last = array_key_last($steps); + $finalClass = ''; + $finalType = Type::VAR; + $nullToken = '__TYPEPHP_NATIVE_NULLSAFE_NULL__'; + + foreach ($steps as $index => $step) { + $nullsafe = $step instanceof Node\Expr\NullsafePropertyFetch + || $step instanceof Node\Expr\NullsafeMethodCall; + if ($nullsafe) { + $body .= $this->getIndent() . 'if (' . $current . ' == nullptr) { return ' + . $nullToken . '; }' . PHP_EOL; + } + + $receiver = new Node\Expr\Variable($current, $step->var->getAttributes()); + if ($step instanceof Node\Expr\PropertyFetch || $step instanceof Node\Expr\NullsafePropertyFetch) { + if (!$step->name instanceof Node\Identifier) { + $this->fatalError($step, 'Dynamic native object property access is not supported'); + } + $access = new Node\Expr\PropertyFetch($receiver, clone $step->name, $step->getAttributes()); + } else { + if (!$step->name instanceof Node\Identifier) { + $this->fatalError($step, 'Dynamic native object method calls are not supported'); + } + $access = new Node\Expr\MethodCall( + $receiver, + clone $step->name, + $step->args, + $step->getAttributes(), + ); + } + + [$value, $before, $after] = $this->parseExprWithCapturedStmts($access); + $valueClass = $this->detectClassOfExpr($access); + $valueType = $this->detectTypeOfExpr($access); + $body .= $this->formatCapturedStmtLines($before); + + if ($index !== $last) { + if (!$this->isNativeObjectClass($valueClass)) { + $this->fatalError( + $step, + 'A Native nullsafe chain cannot continue through a non-Native value', + ); + } + $next = $this->genTmpVarName(); + $this->addLocalVar($next, $this->getNativeObjectPointerType($valueClass)); + $this->addNativeObject($next, $valueClass); + $body .= $this->getIndent() . $next . ' = ' . $value . ';' . PHP_EOL; + $body .= $this->formatCapturedStmtLines($after); + $current = $next; + continue; + } + + $finalClass = $valueClass; + $finalType = $valueType; + if ($finalType === Type::VOID) { + $body .= $this->getIndent() . $value . ';' . PHP_EOL; + $body .= $this->formatCapturedStmtLines($after); + $body .= $this->getIndent() . 'return php::null;' . PHP_EOL; + continue; + } + + if ($after !== []) { + if ($this->isNativeObjectClass($finalClass)) { + $result = $this->genTmpVarName(); + $this->addLocalVar($result, $this->getNativeObjectPointerType($finalClass)); + $this->addNativeObject($result, $finalClass); + $body .= $this->getIndent() . $result . ' = ' . $value . ';' . PHP_EOL; + } else { + $result = $this->genTmpVarName(); + $body .= $this->getIndent() . 'auto ' . $result . ' = ' . $value . ';' . PHP_EOL; + } + $body .= $this->formatCapturedStmtLines($after); + $value = $result; + } + $body .= $this->getIndent() . 'return ' + . ($this->isNativeObjectClass($finalClass) ? $value : 'php::Var(' . $value . ')') + . ';' . PHP_EOL; + } + + $nativeResult = $this->isNativeObjectClass($finalClass); + $returnType = $nativeResult ? $this->getNativeObjectPointerType($finalClass) : Type::VAR; + $nullValue = $nativeResult ? 'nullptr' : 'php::null'; + $body = str_replace($nullToken, $nullValue, $body); + return '[&]() -> ' . $returnType . ' {' . PHP_EOL + . $body + . $this->getIndent() . '}()'; + } + protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef { while ($class !== '' && $this->hasClass($class)) { @@ -486,6 +796,11 @@ trait NativeClassSupportTrait if ($classDef->hasMethod($method)) { return $classDef->getMethod($method); } + if ($classDef->hasAbstractMethod($method) + && isset($classDef->abstractMethodDefs[strtolower($method)]) + ) { + return $classDef->getAbstractMethod($method); + } $class = $classDef->extends; } return null; @@ -542,31 +857,74 @@ trait NativeClassSupportTrait return $resolvedMethod; } - protected function getNativeVirtualMethodName(string $method): string + protected function parseNativeObjectExplicitConversion(NodeAbstract $expr, string $method): ?string + { + $class = $this->detectClassOfExpr($expr); + if (!$this->isNativeObjectClass($class)) { + return null; + } + return $this->parseMethodCall(new Node\Expr\MethodCall( + $expr, + new Node\Identifier($method), + [], + $expr->getAttributes(), + )); + } + + protected function getNativeVirtualMethodName(string|ClassDef $slotClass, string $method): string { - return '__typephp_virtual_' . strtolower($method); + return '__typephp_virtual_' . strtolower($this->getNativeObjectCppName($slotClass)) + . '__' . strtolower($method); } + /** Whether this declaration owns a virtual dispatch slot. */ protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool { - if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL | Modifiers::ABSTRACT)) { + if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL)) { return false; } + if ($method->flags & Modifiers::ABSTRACT) { + return true; + } if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) { return false; } if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) { return true; } - $parent = $class->extends; - while ($parent !== '' && $this->hasClass($parent)) { - $parentDef = $this->getClass($parent); - if ($parentDef->hasMethod($method->name)) { - return true; + return false; + } + + /** + * Return every virtual slot a declaration must implement. PHP permits + * contravariant parameters and covariant returns, so one C++ virtual + * signature cannot represent the whole family. Each source declaration + * owns a stable slot; an override supplies adapters for all ancestor slots. + * + * @return list + */ + protected function getNativeVirtualMethodSlots(ClassDef $class, MethodDef $method): array + { + $slots = []; + $current = $class; + while (true) { + $declaration = null; + if ($current->hasMethod($method->name)) { + $declaration = $current->getMethod($method->name); + } elseif ($current->hasAbstractMethod($method->name) + && isset($current->abstractMethodDefs[strtolower($method->name)]) + ) { + $declaration = $current->getAbstractMethod($method->name); } - $parent = $parentDef->extends; + if ($declaration !== null && $this->isNativeVirtualMethod($current, $declaration)) { + $slots[] = [$current, $declaration]; + } + if ($current->extends === '' || !$this->isNativeObjectClass($current->extends)) { + break; + } + $current = $this->getClass($current->extends); } - return false; + return $slots; } protected function getNativeMethodReturnCppType(FunctionDef $function): string @@ -576,30 +934,65 @@ trait NativeClassSupportTrait : ($this->getNativeObjectReturnType($function) ?? $function->returnType); } - protected function getNativeMethodParameterDeclarations(FunctionDef $function): string + protected function getNativeMethodParameterDeclarations( + FunctionDef $function, + ?int $parameterCount = null, + ): string { $args = []; - foreach ($function->argInfoList as $argument) { + $arguments = $parameterCount === null + ? $function->argInfoList + : array_slice($function->argInfoList, 0, $parameterCount); + foreach ($arguments as $argument) { if ($argument->variadic) { - $args[] = Type::ARRAY . ' ' . $argument->name; + $declaration = Type::ARRAY . ' ' . $argument->name; } else { - $args[] = $this->genArgumentDeclaration($argument); + $declaration = $this->genArgumentDeclaration($argument); } + $args[] = $declaration; } return implode(', ', $args); } + /** + * C++ binds a default argument from the receiver's static type, while PHP + * uses the default declared by the dynamically selected override. Emit an + * overload for every positional arity instead of putting C++ defaults on + * a virtual declaration. Each override adapter can then call its concrete + * php_* function with the supplied prefix and let that declaration provide + * the correct dynamic defaults. + * + * @return list + */ + protected function getNativeVirtualMethodArities(FunctionDef $function): array + { + $total = count($function->argInfoList); + return range(min($function->argCountRequired, $total), $total); + } + protected function getNativeObjectPropertyType(PropertyDef $property): string { if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { return $this->getNativeObjectPointerType($property->class); } return match ($property->type) { - Type::STREAM, Type::BOX => Type::VAR, + // These language values use PHPX's boxed Variant ABI. Embedding + // the implementation classes by value would be incompatible with + // every arithmetic/conversion helper, all of which accepts and + // returns Variant while preserving immutable value semantics. + Type::STREAM, Type::BOX, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL => Type::VAR, default => $property->type, }; } + protected function getNativeObjectInitializerName(string|ClassDef $class): string + { + // Keep compiler-owned helpers outside the php_* user symbol namespace. + // A PHP method named initialize() previously collided with + // php___initialize and produced duplicate C++ definitions. + return 'typephp_native_initialize_fields__' . $this->getNativeObjectCppName($class); + } + protected function getNativeObjectPropertyCppName( string|PropertyDef $property, string|ClassDef|null $declaringClass = null, @@ -740,20 +1133,35 @@ trait NativeClassSupportTrait } $code .= ';' . PHP_EOL; } - foreach ($class->methods as $method) { - if (!$this->isNativeVirtualMethod($class, $method)) { - continue; + foreach ([...$class->methods, ...$class->abstractMethodDefs] as $method) { + foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) { + $ownsSlot = $this->isSameClassName( + $slotClass->getNamespacedName(false), + $class->getNamespacedName(false), + ); + foreach ($this->getNativeVirtualMethodArities($slotMethod->functionDef) as $arity) { + $code .= ' virtual ' . $this->getNativeMethodReturnCppType($slotMethod->functionDef) + . ' ' . $this->getNativeVirtualMethodName($slotClass, $method->name) . '(' + . $this->getNativeMethodParameterDeclarations($slotMethod->functionDef, $arity) . ')' + . ($ownsSlot ? '' : ' override') + . (($method->flags & Modifiers::ABSTRACT) ? ' = 0' : '') + . ';' . PHP_EOL; + } + } + } + if ($this->nativeObjectUsesVirtualClone($class)) { + $code .= ' virtual ' . $name . ' *' . self::NATIVE_VIRTUAL_CLONE_METHOD . '() const'; + if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) { + $code .= ' override'; + } + if ($class->isAbstract()) { + $code .= ' = 0'; } - $parentHasMethod = $class->extends !== '' - && $this->hasClass($class->extends) - && $this->getClass($class->extends)->hasMethod($method->name); - $code .= ' virtual ' . $this->getNativeMethodReturnCppType($method->functionDef) - . ' ' . $this->getNativeVirtualMethodName($method->name) . '(' - . $this->getNativeMethodParameterDeclarations($method->functionDef) . ')' - . ($parentHasMethod ? ' override' : '') . ';' . PHP_EOL; + $code .= ';' . PHP_EOL; } $code .= '};' . PHP_EOL; - $code .= 'void ' . $name . '__initialize(' . $name . ' &object);' . PHP_EOL; + $code .= 'void ' . $this->getNativeObjectInitializerName($class) + . '(' . $name . ' &object);' . PHP_EOL; $code .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL; $code .= 'extern const php::NativeTypeDescriptor ' . $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL; @@ -766,9 +1174,11 @@ trait NativeClassSupportTrait $cpp = $this->getNativeObjectCppName($class); $prefix = $cpp . '__gc'; $code = ''; - $code .= 'void ' . $cpp . '__initialize(' . $cpp . ' &this_) {' . PHP_EOL; + $code .= 'void ' . $this->getNativeObjectInitializerName($class) + . '(' . $cpp . ' &this_) {' . PHP_EOL; if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) { - $code .= ' ' . $this->getNativeObjectCppName($class->extends) . '__initialize(this_);' . PHP_EOL; + $code .= ' ' . $this->getNativeObjectInitializerName($class->extends) + . '(this_);' . PHP_EOL; } foreach ($class->properties as $property) { if ($property->isStatic() || $property->default === null) { @@ -783,21 +1193,45 @@ trait NativeClassSupportTrait } $code .= '}' . PHP_EOL . PHP_EOL; foreach ($class->methods as $method) { - if (!$this->isNativeVirtualMethod($class, $method)) { - continue; - } - $function = $method->functionDef; - $returnType = $this->getNativeMethodReturnCppType($function); - $args = array_map(static fn (ArgInfo $arg): string => $arg->name, $function->argInfoList); $nativeFunction = self::PREFIX . $this->getNativeName( $method->name, $class->namespace, $class->name, ); - $code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name) - . '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL; - $call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; - $code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; + foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) { + $slotFunction = $slotMethod->functionDef; + $returnType = $this->getNativeMethodReturnCppType($slotFunction); + foreach ($this->getNativeVirtualMethodArities($slotFunction) as $arity) { + $args = array_map( + static fn (ArgInfo $arg): string => $arg->name, + array_slice($slotFunction->argInfoList, 0, $arity), + ); + $code .= $returnType . ' ' . $cpp . '::' + . $this->getNativeVirtualMethodName($slotClass, $method->name) + . '(' . $this->getNativeMethodParameterDeclarations($slotFunction, $arity) . ') {' . PHP_EOL; + $call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; + $code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + } + } + } + + if ($this->nativeObjectUsesVirtualClone($class) && !$class->isAbstract()) { + $initializer = ''; + $cloneMethod = $this->findNativeObjectMethod($class->getNamespacedName(false), '__clone'); + if ($cloneMethod !== null) { + $declaringClass = $this->getClass($cloneMethod->functionDef->declaringClass); + $clone = self::PREFIX . $this->getNativeName( + '__clone', + $declaringClass->namespace, + $declaringClass->name, + ); + $initializer = $clone . '(this_); '; + } + $code .= $cpp . ' *' . $cpp . '::' . self::NATIVE_VIRTUAL_CLONE_METHOD . '() const {' . PHP_EOL; + $code .= ' return php::nativeClone<' . $cpp . '>(' + . $this->getNativeObjectDescriptorName($class) . ', *this, ' + . '[&](auto &this_) { ' . $initializer . '});' . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL; } $code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL; diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index cda2e023..d8b4c2f7 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -165,9 +165,9 @@ trait FuncCallOptimizer 'is_bool' => ['constFold' => self::FOLD_SSA_TYPE, 'constFoldExtra' => Type::BOOL], // Custom handlers - 'is_null' => ['handler' => 'genIsNull'], - 'get_class' => ['handler' => 'genGetClassOptimized'], - 'get_parent_class' => ['handler' => 'genGetParentClass'], + 'is_null' => ['handler' => 'genIsNull', 'nativeReceiver' => true], + 'get_class' => ['handler' => 'genGetClassOptimized', 'nativeReceiver' => true], + 'get_parent_class' => ['handler' => 'genGetParentClass', 'nativeReceiver' => true], 'function_exists' => ['handler' => 'genFunctionExistsOptimized'], 'func_get_arg' => ['handler' => 'genFuncGetArgOptimized'], 'func_get_args' => ['handler' => 'genFuncGetArgsOptimized'], @@ -177,7 +177,9 @@ trait FuncCallOptimizer 'array_keys' => ['handler' => 'genArrayKeys'], 'array_key_exists' => ['handler' => 'genArrayKeyExists'], 'round' => ['handler' => 'genRound'], - 'count' => ['handler' => 'genCount'], + // count() is also a language-level Native operation when its + // concrete receiver implements Countable. + 'count' => ['handler' => 'genCount', 'nativeReceiver' => true], 'define' => ['handler' => 'genDefine'], 'is_callable' => ['handler' => 'genIsCallable'], ]; @@ -216,10 +218,13 @@ trait FuncCallOptimizer // language-level Native keyword aliases and are lowered to an exact // Native method; every other PHP function rejects Native pointers. if (!isset($config['conversion'])) { - foreach ($expr->args as $arg) { + foreach ($expr->args as $index => $arg) { if ($arg instanceof Node\Arg && $this->isNativeObjectClass($this->detectClassOfExpr($arg->value)) ) { + if (($config['nativeReceiver'] ?? false) && $index === 0) { + continue; + } $this->fatalError( $arg, 'Native objects cannot cross a dynamic PHP/ZendVM call boundary', @@ -648,7 +653,11 @@ trait FuncCallOptimizer protected function genIsNull(string $n, Node\Expr\FuncCall $e, array $c): string { - return '(' . $this->parseExprAsValue($e->args[0]->value) . ').isNull()'; + $value = $e->args[0]->value; + if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) { + return '(' . $this->parseExprAsValue($value) . ' == nullptr)'; + } + return '(' . $this->parseExprAsValue($value) . ').isNull()'; } protected function genIsCallable(string $n, Node\Expr\FuncCall $e, array $c): string|false @@ -659,9 +668,24 @@ trait FuncCallOptimizer return $this->dispatchFuncCall('is_callable', $e, ['target' => 'php::fn::is_callable']); } - protected function genGetClassOptimized(string $n, Node\Expr\FuncCall $e, array $c): string + protected function genGetClassOptimized(string $n, Node\Expr\FuncCall $e, array $c): string|false { + if ($e->args === []) { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $e, + 'Native classes do not support runtime class introspection; use `self::class` or a concrete class name', + ); + } + return false; + } $obj = $e->args[0]->value; + if ($this->isNativeObjectClass($this->detectClassOfExpr($obj))) { + $this->fatalError( + $e, + 'Native classes do not support runtime class introspection; use `NativeClass::class`', + ); + } if ($this->isVarExpr($obj) && $this->isTypedObject($obj->name)) { return $this->getLiteralString($this->getObjectType($obj->name)); } @@ -671,12 +695,24 @@ trait FuncCallOptimizer protected function genGetParentClass(string $n, Node\Expr\FuncCall $e, array $c): string { if (count($e->args) === 0) { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $e, + 'Native classes do not support runtime class introspection; use `parent::class` or a concrete class name', + ); + } if ($this->classDef && $this->classDef->extends) { return $this->getLiteralString($this->classDef->extends); } return 'false'; } $arg = $e->args[0]->value; + if ($this->isNativeObjectClass($this->detectClassOfExpr($arg))) { + $this->fatalError( + $e, + 'Native classes do not support runtime class introspection; use a concrete class name', + ); + } if ($this->isScalarString($arg)) { $cls = $this->getClass($arg->value); if ($cls && $cls->extends) return $this->getLiteralString($cls->extends); @@ -753,6 +789,29 @@ trait FuncCallOptimizer protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string { + $receiver = $e->args[0] ?? null; + $nativeClass = $receiver instanceof Node\Arg + ? $this->detectClassOfExpr($receiver->value) + : ''; + if ($this->isNativeObjectClass($nativeClass)) { + if (count($e->args) !== 1) { + $this->fatalError($e, 'count() accepts exactly one argument for native objects'); + } + if (!$this->isObjectClassStaticallyAssignableTo($nativeClass, 'Countable')) { + $this->fatalError($e, 'count() requires a native class implementing Countable'); + } + + // Native objects have no zend_class_entry/count_elements handler. + // Countable gives us an exact compile-time target, so lower this + // directly and retain the same zero-cost call path as $obj->count(). + return $this->parseMethodCall(new Node\Expr\MethodCall( + $receiver->value, + new Node\Identifier('count'), + [], + $e->getAttributes(), + )); + } + $folded = $this->doFoldCountLiteral($e); if ($folded !== false) return $folded; if (count($e->args) >= 2) { @@ -833,8 +892,14 @@ trait FuncCallOptimizer $funcName = $expr->args[0]->value; if ($this->isScalarString($funcName)) { $nameLower = strtolower(trim($funcName->value, '\\')); - if ($this->findNativeFunction($nameLower)) { - return 'true'; + $nativeFunction = $this->findNativeFunction($nameLower); + if ($nativeFunction) { + // A function whose ABI contains Native pointers is callable + // only from generated TypePHP C++. It has no Zend wrapper and + // therefore must remain invisible to function_exists(). + return $this->functionUsesNativeObject($this->getFunction($nativeFunction)) + ? 'false' + : 'true'; } $funcName = $this->getLiteralString($nameLower); return 'php::fn::function_exists(' . $funcName . ')'; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 192c5288..d551a3c0 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -238,7 +238,7 @@ trait AssignOpTrait || $this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName); if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { - $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass); + $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); } } elseif ($left instanceof Expr\PropertyFetch && $this->isVarExpr($left->var) @@ -263,6 +263,15 @@ trait AssignOpTrait && $property->type === Type::OBJECT && $this->isNativeObjectClass($property->class); } + } elseif ($left instanceof Expr\ArrayDimFetch && $this->isStdContainerExpr($left)) { + $info = $this->isStdArrayExpr($left) + ? $this->getStdArrayInfo($left) + : $this->getStdContainerInfo($left); + // A std container with a concrete Native class element type is + // compile-time storage, not a PHP array/Variant boundary. + $allowed = $info !== null + && isset($info['class']) + && $this->isNativeObjectClass($info['class']); } if (!$allowed) { $this->fatalError( @@ -350,6 +359,10 @@ trait AssignOpTrait // 类型推断,获取对象的类名,如果不是对象则返回空字符串 $rightClass = $this->detectClassOfExpr($right); if ($this->isNativeObjectVar($var)) { + // Assignment rebinds the local pointer slot. Even an + // assignment nested in a conditional invalidates the simple + // non-null proof; later reads fall back to nativeDeref(). + $this->forgetNativeObjectNonNull($var); $leftClass = $this->getNativeObjectVarClass($var); if ($this->isNull($right)) { return $var . ' = nullptr'; @@ -436,6 +449,7 @@ trait AssignOpTrait } } elseif ($this->isVarExpr($right)) { $rightVar = $this->parseIdentifier($right); + $this->assertStdContainerDoesNotEscapeNativeObjects($right, $rightVar); $type = $this->isStdContainer($rightVar) ? Type::ARRAY : $this->getVarType($rightVar); $finalVarType = $this->getNormalAssignType($type); $leftClass = $this->getDeclaredObjectType($var); @@ -598,6 +612,7 @@ trait AssignOpTrait protected function parseAssignOp(Expr\AssignOp $node, string $op): string { $this->assertNotNullsafeWriteContext($node->var); + $this->assertNativePropertyHookDirectWriteTarget($node->var); $pythonOperator = $this->parsePythonAssignOperator($node); if ($pythonOperator !== null) { return $pythonOperator; @@ -605,6 +620,13 @@ trait AssignOpTrait $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); $this->guardLiteralDivisionByZero($node->expr, $op); + if ($node->var instanceof Expr\PropertyFetch && $this->isNativeObjectPropertyHook($node->var)) { + $this->fatalError( + $node->var, + 'Native property hooks only support direct reads and assignments', + ); + } + if ($node->var instanceof Expr\PropertyFetch && $this->isReadOnlyPropertyHook($node->var)) { $this->fatalError($node->var, 'Cannot write to read-only hooked property'); } @@ -754,6 +776,21 @@ trait AssignOpTrait } $rightType = $this->detectTypeOfExpr($node->expr); + if (in_array($def->type, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT], true)) { + $binaryOp = $this->removeAssignOp($op); + $leftExpr = $this->parseWritableIdentifier($node->var); + $rightExpr = (string) $this->parseIdentifier($node->expr); + $value = $this->parseBigAssignOpExpr( + $leftExpr, + $def->type, + $rightExpr, + $rightType, + $binaryOp, + $node->var, + $node->expr, + ); + return $leftExpr . ' = ' . $value; + } if ($this->isFixedObjectProp($def) && $rightType !== Type::VAR && !$this->canAssignStaticTypeToObjectProperty($def, $rightType)) { $this->fatalError( $node->var, @@ -906,10 +943,15 @@ trait AssignOpTrait protected function parseAssignRef(Expr\AssignRef $expr): string { $this->assertNotNullsafeWriteContext($expr->var); + $this->assertNativePropertyHookDirectWriteTarget($expr->var); + $this->assertNativePropertyHookDirectWriteTarget($expr->expr); if ($expr->expr instanceof Expr\NullsafePropertyFetch) { $this->fatalError($expr->expr, 'Cannot take reference of a nullsafe chain'); } + $this->assertNativeObjectReferenceForbidden($expr->var, $expr); + $this->assertNativeObjectReferenceForbidden($expr->expr, $expr); + // A reference would outlive the constructor-only write window and // make later mutations invisible to the compiler. It is therefore // forbidden on either side even inside the declaring constructor. @@ -1015,6 +1057,7 @@ trait AssignOpTrait protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string { + $this->assertNativePropertyHookDirectWriteTarget($left); $propertyWriteTarget = $this->preparePropertyWriteTarget($left->var); $code = ''; $value = $this->parseExprAsValue($right); diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index f1ea7c38..57a7ff0d 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -50,6 +50,12 @@ trait ClassConstantFetchTrait $const = $this->escapeString($this->parseIdentifier($expr->name)); if ($class === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $expr, + 'Native classes do not support late static binding; use `self::` or a concrete class name', + ); + } if (!$this->methodDef) { $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); } @@ -116,6 +122,12 @@ trait ClassConstantFetchTrait $class = $this->parseIdentifier($expr->class); if ($class === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $expr, + 'Native classes do not support late static binding; use `self::` or a concrete class name', + ); + } if (!$this->methodDef) { $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); } diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index db4d58fc..3b39777e 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -99,6 +99,25 @@ trait FunctionCallTrait } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { $name = $this->parseIdentifier($expr->name); $globalName = ltrim($name, '\\'); + if ($globalName === 'get_called_class' && $this->classDef?->nativeObject) { + $this->fatalError( + $expr, + 'Native classes do not support late static binding; use `self::class` or a concrete class name', + ); + } + if (($globalName === 'get_class' || $globalName === 'get_parent_class') + && (($expr->args === [] && $this->classDef?->nativeObject) + || ($expr->args !== [] + && $this->isNativeObjectClass($this->detectClassOfExpr($expr->args[0]->value)))) + ) { + $replacement = $globalName === 'get_class' + ? '`self::class` or a concrete class name' + : '`parent::class` or a concrete class name'; + $this->fatalError( + $expr, + "Native classes do not support runtime class introspection; use {$replacement}", + ); + } if ($this->isInternalFunction($globalName)) { $this->assertWasiFunctionSupported($expr, $globalName); $this->markInternalFunctionCallbackCall($globalName, $expr->args); @@ -110,7 +129,20 @@ trait FunctionCallTrait if (count($expr->args) !== 1 || $expr->args[0]->unpack) { $this->fatalError($expr, 'The any function expects exactly one non-unpacked argument'); } - return $this->parseExprAsValue($expr->args[0]->value); + $value = $expr->args[0]->value; + if ($this->isNativeObjectClass($this->detectClassOfExpr($value))) { + $this->fatalError( + $value, + 'Native objects cannot be converted to mixed with any(); use an explicitly typed Native variable', + ); + } + if ($this->isVarExpr($value)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $value, + $this->parseIdentifier($value), + ); + } + return $this->parseExprAsValue($value); } if ($globalName === 'expected' || $globalName === 'unexpected') { if (count($expr->args) !== 1 || $expr->args[0]->unpack) { @@ -125,6 +157,11 @@ trait FunctionCallTrait $nativeFn = $this->findNativeFunction($name); if ($nativeFn) { $expr->setAttribute('nativeCall', $nativeFn); + if ($expr->isFirstClassCallable() + && $this->functionUsesNativeObject($this->getFunction($nativeFn)) + ) { + $this->fatalError($expr, 'Native ABI functions cannot be converted to Zend closures'); + } // 函数调用占位符,不是真实的函数调用 if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { return $this->genPlaceHolder($this->identifierToStr($expr->name)); diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 88d391d0..804b492b 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -406,14 +406,23 @@ trait MethodCallTrait } if ($class !== '' && $this->isNativeObjectClass($class)) { + if ($expr->isFirstClassCallable()) { + $this->fatalError($expr, 'Native object methods cannot be converted to Zend closures'); + } if (!$this->isNamedMethod($expr->name)) { $this->fatalError($expr, 'Dynamic native object method calls are not supported'); } $nativeMethodName = strtolower($expr->name->toString()); + if ($nativeMethodName === '__construct') { + $this->fatalError($expr, 'Explicit calls to native object constructors are not supported'); + } if ($nativeMethodName === '__destruct') { $this->fatalError($expr, 'Explicit calls to native object destructors are not supported'); } $nativeKeyword = $expr->name->toString(); + if ($nativeKeyword === 'toRef') { + $this->assertNativeObjectReferenceForbidden($expr->var, $expr); + } if (isset(self::KEYWORD_METHOD_MAP[$nativeKeyword])) { if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$nativeKeyword]) && $expr->args !== []) { $this->fatalError($expr, "The {$nativeKeyword} method does not accept parameters"); @@ -505,21 +514,47 @@ trait MethodCallTrait // Native objects have their own C++ virtual thunk for an // overridden family; do not let the Zend-object devirtualizer // downgrade this call to the dynamic path. + $nativeMethodDef = $this->findNativeObjectMethod($class, $methodName); $nativeFunc = $this->getNativeMethod($expr, $class, $methodName); if ($nativeFunc === false) { - $this->fatalError($expr, "Native class `{$class}` has no method `{$methodName}()`"); + if ($nativeMethodDef === null || !($nativeMethodDef->flags & Modifiers::ABSTRACT)) { + $this->fatalError($expr, "Native class `{$class}` has no method `{$methodName}()`"); + } + $declaringClassName = $nativeMethodDef->functionDef->declaringClass; + $declaringClass = $this->getClass($declaringClassName); + if (!$this->checkAccessible($declaringClass, $nativeMethodDef->flags)) { + $this->fatalError( + $expr, + "Method `{$declaringClassName}::{$methodName}()` is not accessible", + ); + } + $nativeFunc = $this->getNativeName( + $methodName, + $declaringClass->namespace, + $declaringClass->name, + ); + $this->checkNativeCallArgs( + $expr, + $nativeMethodDef->functionDef, + $expr->args, + $declaringClassName . '::' . $methodName, + ); } $expr->setAttribute('nativeCall', $nativeFunc); $nativeFunctionDef = $this->getFunction($nativeFunc); $declaringClass = $this->getClass($nativeFunctionDef->declaringClass); - $declaringMethod = $declaringClass->getMethod($methodName); + $declaringMethod = $nativeMethodDef ?? $declaringClass->getMethod($methodName); if ($this->isNativeVirtualMethod($declaringClass, $declaringMethod)) { $call = $this->getNativeObjectMemberReceiver($object) - . $this->getNativeVirtualMethodName($methodName); + . $this->getNativeVirtualMethodName($declaringClass, $methodName); if ($expr->args === []) { return $call . '()'; } - return $call . '(' . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + return $call . '(' . $this->parseNativeCallArgs( + $expr->args, + $nativeFunc, + deferTrailingDefaults: true, + ) . ')'; } $receiver = $this->getNativeObjectReceiver($object); if ($expr->args === []) { @@ -756,6 +791,12 @@ trait MethodCallTrait } $placeHolder = $fn; } elseif ($this->isNameExpr($expr->class) and $class === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $expr, + 'Native classes do not support late static binding; use `self::` or a concrete class name', + ); + } $method = $this->parseIdentifier($expr->name); $methodPtr = $this->identifierToStr($expr->name, literal: true); $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index eb98bd34..cff6e088 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -38,6 +38,11 @@ trait NullsafeAccessTrait Expr\PropertyFetch|Expr\MethodCall|Expr\NullsafePropertyFetch|Expr\NullsafeMethodCall $expr ): string { + $native = $this->parseNativeNullsafeAccess($expr); + if ($native !== null) { + return $native; + } + $list = []; $ownedTmpVars = []; $comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index a7c99be9..68db2891 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -415,6 +415,12 @@ trait PropertyAccessTrait return $this->getLiteralString($this->classDef->extends); } if ($name === 'static') { + if ($this->classDef?->nativeObject) { + $this->fatalError( + $class, + 'Native classes do not support late static binding; use `self::` or a concrete class name', + ); + } if (!$this->methodDef) { $this->fatalError($class, "The 'static' keyword can only be used as the class name in class methods"); } @@ -618,6 +624,12 @@ trait PropertyAccessTrait return $rightExpr; } + if ($def->type === Type::STREAM) { + return $this->detectTypeOfExpr($right) === Type::STREAM + ? $rightExpr + : 'php::toStream(' . $rightExpr . ')'; + } + $typeCheck = $this->getObjectPropertyAssignTypeCheck($def); if (empty($typeCheck)) { return $rightExpr; @@ -768,6 +780,7 @@ trait PropertyAccessTrait $lines = []; foreach ($vars as $var) { $this->assertNotNullsafeWriteContext($var); + $this->assertNativePropertyHookDirectWriteTarget($var); if ($this->isArrayDimFetch($var)) { if ($var->dim === null) { $this->fatalError($var, 'Cannot use [] for array unset'); @@ -835,6 +848,7 @@ trait PropertyAccessTrait } $type = $this->getVarType($name); if ($this->isNativeObjectVar($name)) { + $this->forgetNativeObjectNonNull($name); $lines[] = "{$name} = nullptr;"; } elseif ($this->isNativeType($type)) { $this->warning($var, "Variable of native type `\${$name}` cannot be unset"); @@ -957,6 +971,7 @@ trait PropertyAccessTrait if ($this->isNativeObjectClass($nativeExpressionClass)) { $objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass); + $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); return $this->getNativeObjectMemberReceiver($objectName) . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); } @@ -976,6 +991,7 @@ trait PropertyAccessTrait if ($resolution === null) { $this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`"); } + $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR); return $this->getNativeObjectMemberReceiver($objectName) . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); } @@ -1040,6 +1056,38 @@ trait PropertyAccessTrait return $this->getNativePropertyDef($expr)?->setter; } + protected function isNativeObjectPropertyHook(NodeAbstract $expr): bool + { + $class = $this->getNativePropertyClassDef($expr); + $property = $this->getNativePropertyDef($expr); + return $class?->nativeObject === true + && $property !== null + && ($property->getter !== null || $property->setter !== null); + } + + /** + * Native hooks lower to ordinary getter/setter calls. An indirect write + * would only mutate the value returned by the getter and cannot reliably + * invoke the setter, so only a direct property assignment is meaningful. + */ + protected function assertNativePropertyHookDirectWriteTarget(NodeAbstract $expr): void + { + while ($expr instanceof Expr\ArrayDimFetch) { + $expr = $expr->var; + } + if ($expr instanceof Expr\PropertyFetch && $this->isIdExpr($expr->name)) { + // Resolve the property before querying its hook metadata. Indirect + // write paths reach this guard before normal property lowering. + $this->getPropertyIdentifier($expr, $expr->var, $expr->name); + } + if ($expr instanceof Expr\PropertyFetch && $this->isNativeObjectPropertyHook($expr)) { + $this->fatalError( + $expr, + 'Native property hooks only support direct reads and assignments', + ); + } + } + protected function isReadOnlyPropertyHook(NodeAbstract $expr): bool { if ($this->isPropertyHookBackingAccess($expr)) { diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index e70c8445..7f1995b9 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -95,6 +95,27 @@ trait StdContainerTrait return $this->context->stdContainers[$var]; } + protected function getStdContainerNativeObjectClass(string $var): string + { + if (!$this->isStdContainer($var)) { + return ''; + } + $class = $this->getStdContainerVarInfo($var)['class'] ?? ''; + return is_string($class) && $this->isNativeObjectClass($class) ? $class : ''; + } + + protected function assertStdContainerDoesNotEscapeNativeObjects( + NodeAbstract $node, + string $var, + ): void { + if ($this->getStdContainerNativeObjectClass($var) !== '') { + $this->fatalError( + $node, + 'Std containers holding Native objects cannot cross a PHP/ZendVM value boundary', + ); + } + } + protected function getStdContainerKeyType(string $var): string { if ($this->isStdVector($var) or $this->isStdArray($var)) { @@ -107,7 +128,14 @@ trait StdContainerTrait { $info = $this->getStdContainerVarInfo($var); if ($this->isStdArray($var)) { - return count($info['sizes']) > 1 ? Type::ARRAY : $info['type']; + if (count($info['sizes']) > 1) { + return Type::ARRAY; + } + } + if ($info['type'] === Type::OBJECT && $this->isNativeObjectClass($info['class'] ?? '')) { + $this->addNativeObject($valueVar, $info['class']); + unset($this->context->objects[$valueVar]); + return $this->getNativeObjectPointerType($info['class']); } if ($info['type'] === Type::OBJECT and $info['class']) { $this->addObject($valueVar, $info['class']); @@ -117,10 +145,10 @@ trait StdContainerTrait return $info['type']; } - protected function getStdArrayDecl(string $type, array $sizes): string + protected function getStdArrayDecl(string $type, array $sizes, ?string $class = null): string { $decl = str_repeat(Type::STD_ARRAY . '<', count($sizes)); - $decl .= $this->getStdContainerElementType($type); + $decl .= $this->getStdContainerElementType($type, $class); for ($i = count($sizes) - 1; $i >= 0; $i--) { $decl .= ', ' . $sizes[$i] . '>'; } @@ -146,7 +174,7 @@ trait StdContainerTrait $nestedSizes = array_slice($sizes, $accessLevel); return [ 'kind' => 'array', - 'decl' => $this->getStdArrayDecl($info['type'], $nestedSizes), + 'decl' => $this->getStdArrayDecl($info['type'], $nestedSizes, $info['class']), 'type' => $info['type'], 'class' => $info['class'], 'sizes' => array_reverse($nestedSizes), @@ -584,8 +612,11 @@ trait StdContainerTrait return $container . '_ref.offsetUnset(' . $index . ')'; } - protected function getStdContainerElementType(string $type): string + protected function getStdContainerElementType(string $type, ?string $class = null): string { + if ($class !== null && $this->isNativeObjectClass($class)) { + return $this->getNativeObjectPointerType($class); + } return match ($type) { Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL, Type::STREAM, Type::BOX => Type::VAR, default => $type, @@ -678,6 +709,20 @@ trait StdContainerTrait } return $this->convertExprFromType($targetType, $valueExpr); } + if ($this->isNativeObjectClass($class)) { + if ($this->isNull($expr)) { + return 'nullptr'; + } + $rightClass = $this->detectClassOfExpr($expr); + if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $class)) { + $actual = $rightClass === '' ? $this->detectTypeOfExpr($expr) : $rightClass; + $this->fatalError( + $expr, + "Cannot assign value of type `{$actual}` to std container value of native class `{$class}`", + ); + } + return $valueExpr; + } $rightClass = $this->detectClassOfExpr($expr); if ($rightClass !== '') { if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $class)) { @@ -768,9 +813,15 @@ trait StdContainerTrait $this->fatalError($expr, "{$owner} key only supports Type::Int or Type::String"); } - protected function getStdMapDecl(string $containerType, string $keyType, string $valueType): string + protected function getStdMapDecl( + string $containerType, + string $keyType, + string $valueType, + ?string $class = null, + ): string { - return $containerType . '<' . $keyType . ', ' . $this->getStdContainerElementType($valueType) . '>'; + return $containerType . '<' . $keyType . ', ' + . $this->getStdContainerElementType($valueType, $class) . '>'; } protected function parseStdArray(string $var, Expr\StaticCall $expr): string @@ -807,7 +858,7 @@ trait StdContainerTrait } $totalBytes = array_product($nesting) * $byte; - $decl = $this->getStdArrayDecl($type, $nesting); + $decl = $this->getStdArrayDecl($type, $nesting, $typeInfo['class']); $this->context->stdArrays[$var] = $this->addStdTypeId([ 'kind' => 'array', 'decl' => $decl, @@ -833,7 +884,8 @@ trait StdContainerTrait } $size = $expr->args[1]->value->value; } - $decl = Type::STD_VECTOR . '<' . $this->getStdContainerElementType($type) . '>'; + $decl = Type::STD_VECTOR . '<' + . $this->getStdContainerElementType($type, $typeInfo['class']) . '>'; $this->context->stdContainers[$var] = $this->addStdTypeId([ 'kind' => 'vector', 'decl' => $decl, @@ -862,7 +914,7 @@ trait StdContainerTrait $keyType = $this->parseStdMapKeyType($expr->args[0]->value, $funcName); $valueTypeInfo = $this->parseStdValueTypeInfo($expr->args[1]->value, $funcName); $valueType = $valueTypeInfo['type']; - $decl = $this->getStdMapDecl($containerType, $keyType, $valueType); + $decl = $this->getStdMapDecl($containerType, $keyType, $valueType, $valueTypeInfo['class']); $this->context->stdContainers[$var] = $this->addStdTypeId([ 'kind' => $kind, 'decl' => $decl, diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 03222e5d..70487131 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -264,6 +264,7 @@ trait TypeConversionTrait protected function convertToRef(NodeAbstract $expr): string { + $this->assertNativeObjectReferenceForbidden($expr, $expr); $this->checkLeftValue($expr); $var = $this->parseIdentifier($expr); if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) { diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index d2e1e868..6d7dc76e 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -45,6 +45,10 @@ trait UnaryExpressionTrait protected function parseCastInt(Expr\Cast\Int_ $node): string { $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); + $native = $this->parseNativeObjectExplicitConversion($node->expr, 'toInt'); + if ($native !== null) { + return $native; + } return $this->convertIntExpr( $this->parseExprAsValue($node->expr), $this->detectTypeOfExpr($node->expr) @@ -60,6 +64,10 @@ trait UnaryExpressionTrait protected function parseCastBool(Expr\Cast\Bool_ $node): string { $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); + $native = $this->parseNativeObjectExplicitConversion($node->expr, 'toBool'); + if ($native !== null) { + return $native; + } return $this->convertBoolExpr( $this->parseExprAsValue($node->expr), $this->detectTypeOfExpr($node->expr) @@ -69,6 +77,9 @@ trait UnaryExpressionTrait protected function parseCastObject(Expr\Cast\Object_ $node): string { $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); + if ($this->isNativeObjectClass($this->detectClassOfExpr($node->expr))) { + $this->fatalError($node, 'Native objects cannot be converted to Zend objects'); + } return $this->convertObjectExpr($this->parseExprAsValue($node->expr)); } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index ef58806e..5bcd190c 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -432,9 +432,10 @@ class Preprocessor extends CompilerBase // Record late-bound parameter type keywords so they can be re-resolved // to the consuming class when a trait method is flattened into a class. $argInfo->typeKeyword = $typeKeyword; - // Ordinary PHP references use php::Ref at the native ABI. Keep the - // resolved declaration metadata above, however: Native object - // references need it to lower to `native_struct *&` instead. + // Ordinary PHP references use php::Ref at the native ABI. Native + // object references are rejected after the complete signature has + // been parsed: a typed pointer already shares object identity, while + // PHP & would additionally expose caller-slot rebinding. return $param->byRef ? Type::REF : $type; } @@ -661,6 +662,7 @@ class Preprocessor extends CompilerBase } $this->parseParams($v->params, $functionDef); + $this->assertNativeObjectFunctionSignature($v, $functionDef); if ($this->classDef !== null && !$this->classDef->nativeObject @@ -1589,8 +1591,11 @@ class Preprocessor extends CompilerBase if (is_string($traitOrigin)) { $this->methodDef->traitOrigin = $traitOrigin; } - $this->methodDef->functionDef = $this->parseFunctionDecl($v); - $this->methodDef->functionDef->method = true; + // Keep abstract method metadata in the symbol repository as well. + // Native virtual calls need the same argument/default lowering as + // concrete calls, even though no free-function body is emitted. + $this->prepareFunction($v); + $this->methodDef->functionDef->abstractMethod = true; $this->checkRequiredArgNum($name, $this->methodDef, $v); if ($this->method === '__construct') { foreach ($v->params as $param) { diff --git a/src/Translator.php b/src/Translator.php index ef7d8cac..97e4393d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1742,6 +1742,9 @@ CODE; $code .= $this->genDefaultArgumentHelperDeclarations(); foreach ($this->symbols->functions() as $name => $func) { + if ($func->abstractMethod) { + continue; + } $functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func); $list = []; if ($func->method) { @@ -3731,6 +3734,13 @@ CODE; if (!$argInfo->variadic and $argInfo->declaredClass) { $this->addObject($argInfo->name, $argInfo->declaredClass); } + $argumentClass = $argInfo->declaredClass ?: $argInfo->class; + if (!$argInfo->nullable && $this->isNativeObjectClass($argumentClass)) { + // genNativeObjectParameterChecks() establishes this invariant + // once at function entry. Rebinding the local pointer later + // invalidates it conservatively in parseAssign()/parseUnset(). + $this->markNativeObjectNonNull($argInfo->name); + } } if ($this->functionDef->generator) { @@ -3747,6 +3757,7 @@ CODE; $oriTmpVarIndex = $this->context->tmpVarIndex; $oriDeclaredObjects = $this->context->declaredObjects; $oriNativeObjects = $this->context->nativeObjects; + $oriNonNullNativeObjects = $this->context->nonNullNativeObjects; /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ $ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); $ssaBuilder->build(); @@ -3764,6 +3775,7 @@ CODE; $oriTmpVarIndex, $oriDeclaredObjects, $oriNativeObjects, + $oriNonNullNativeObjects, ); } @@ -3918,7 +3930,7 @@ CODE; } } - private function validateMethodOverrideSignature( + protected function validateMethodOverrideSignature( NodeAbstract $v, string $methodName, MethodDef $childMethodDef, @@ -4003,6 +4015,13 @@ CODE; $this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass); } } + + if ($this->classDef?->nativeObject + && $this->hasClass($parentClass) + && $this->getClass($parentClass)->nativeObject + ) { + $this->assertNativeVirtualByRefStorageCompatible($v, $childFuncDef, $parentFuncDef); + } } private function fatalMethodOverrideIncompatible( @@ -4609,7 +4628,7 @@ CODE; } } - private function findClassMethodDef(ClassDef $classDef, string $methodName, bool $includeAbstract = true): ?MethodDef + protected function findClassMethodDef(ClassDef $classDef, string $methodName, bool $includeAbstract = true): ?MethodDef { $current = $classDef; while (true) { diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 3158a5f5..98a879f4 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -64,6 +64,15 @@ trait NativeTypeCompatibilityTrait return true; } $classDef = $this->getClass($class); + if ($classDef->nativeObject + && strcasecmp($expected, 'Stringable') === 0 + && $this->findNativeObjectMethod($class, '__toString') !== null + ) { + // PHP implicitly marks every class with __toString() as + // Stringable. Native classes have no zend_class_entry, so preserve + // that relation in the compile-time class graph instead. + return true; + } while (true) { if ($isInterface) { if ($classDef->implements and in_array($expected, $classDef->implements)) { @@ -148,6 +157,12 @@ trait NativeTypeCompatibilityTrait { $type = $this->detectTypeOfExpr($arg->value); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); + if ($this->isVarExpr($arg->value)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $arg, + $this->parseIdentifier($arg->value), + ); + } if (!empty($argInfo->typeCheck)) { $this->checkCompositeTypeAssignment( @@ -189,16 +204,6 @@ trait NativeTypeCompatibilityTrait "Argument `{$argName}` must be a native object of type `{$declaredClass}`" ); } - if ($argInfo->byRef) { - if (!$this->isVarExpr($arg->value)) { - $this->fatalError($arg, 'Native object reference arguments must be variables'); - } - $var = $this->parseIdentifier($arg->value); - if (!$this->isNativeObjectVar($var)) { - $this->fatalError($arg, 'Native object reference arguments must be typed native variables'); - } - return $var; - } $expr = $this->parseOrderedArg($arg); return $this->materializeCallArgValue($arg->value, $expr); } @@ -206,6 +211,7 @@ trait NativeTypeCompatibilityTrait if ($argInfo->byRef) { if ($this->isReferenceWrapperCall($arg->value)) { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); + $this->assertNativeObjectReferenceForbidden($inner, $arg); $this->assertReadonlyPropertyReferenceForbidden($inner, $arg, false); if ($this->isVarExpr($inner)) { $arg->value = $inner; @@ -217,6 +223,7 @@ trait NativeTypeCompatibilityTrait $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property'); } } else { + $this->assertNativeObjectReferenceForbidden($arg->value, $arg); $this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false); } if ($this->isVarExpr($arg->value)) { diff --git a/tests/compiler/native-class/abstract-method.phpt b/tests/compiler/native-class/abstract-method.phpt new file mode 100644 index 00000000..48a18682 --- /dev/null +++ b/tests/compiler/native-class/abstract-method.phpt @@ -0,0 +1,47 @@ +--TEST-- +Native class: abstract methods dispatch through the native virtual thunk +--FILE-- +value(); + } +} + +function readAbstractValue(NativeAbstractValue $value): int +{ + return $value->value(); +} + +function readAbstractLabel(NativeAbstractValue $value): string +{ + return $value->label(); +} + +function main(): void +{ + $value = new NativeConcreteValue(); + var_dump(readAbstractValue($value)); + var_dump(readAbstractLabel($value)); +} +?> +--EXPECT-- +int(42) +string(11) "concrete=42" diff --git a/tests/compiler/native-class/by-reference.phpt b/tests/compiler/native-class/by-reference.phpt deleted file mode 100644 index 159b10f1..00000000 --- a/tests/compiler/native-class/by-reference.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Native class: by-reference parameters replace the caller pointer ---FILE-- -value = $value; - } -} - -function replaceNativeReference(NativeReferenceValue &$value): void -{ - $value = new NativeReferenceValue(42); -} - -function main(): void -{ - $value = new NativeReferenceValue(1); - replaceNativeReference($value); - var_dump($value->value); -} -?> ---EXPECT-- -int(42) diff --git a/tests/compiler/native-class/clone-expression.phpt b/tests/compiler/native-class/clone-expression.phpt new file mode 100644 index 00000000..3e8f7218 --- /dev/null +++ b/tests/compiler/native-class/clone-expression.phpt @@ -0,0 +1,41 @@ +--TEST-- +Native class: clone accepts rooted native-producing expressions +--FILE-- +number++; + } +} + +#[Native] +class NativeCloneExpressionHolder +{ + public ?NativeCloneExpressionValue $value; +} + +function makeNativeCloneExpressionValue(): NativeCloneExpressionValue +{ + return new NativeCloneExpressionValue(); +} + +function main(): void +{ + $fromCall = clone makeNativeCloneExpressionValue(); + var_dump($fromCall->number); + + $holder = new NativeCloneExpressionHolder(); + $holder->value = makeNativeCloneExpressionValue(); + $fromProperty = clone $holder->value; + var_dump($fromProperty->number); +} +?> +--EXPECT-- +int(41) +int(41) diff --git a/tests/compiler/native-class/conditions.phpt b/tests/compiler/native-class/conditions.phpt new file mode 100644 index 00000000..b4640c34 --- /dev/null +++ b/tests/compiler/native-class/conditions.phpt @@ -0,0 +1,33 @@ +--TEST-- +Native class: object pointers use PHP object truthiness in conditions +--FILE-- + +--EXPECT-- +bool(true) +bool(false) +bool(false) +bool(true) +present diff --git a/tests/compiler/native-class/error-suppress-type.phpt b/tests/compiler/native-class/error-suppress-type.phpt new file mode 100644 index 00000000..5aa595c0 --- /dev/null +++ b/tests/compiler/native-class/error-suppress-type.phpt @@ -0,0 +1,22 @@ +--TEST-- +Native class: error suppression preserves typed pointer identity +--FILE-- +value = 42; + var_dump($first === $second, $first->value); +} +?> +--EXPECT-- +bool(true) +int(42) diff --git a/tests/compiler/native-class/global-and-static.phpt b/tests/compiler/native-class/global-and-static.phpt index 738b6eda..cea1bed0 100644 --- a/tests/compiler/native-class/global-and-static.phpt +++ b/tests/compiler/native-class/global-and-static.phpt @@ -9,6 +9,9 @@ class NativeCounter public int $value; } +#[Native] +class NativeCounterChild extends NativeCounter {} + function initializeGlobal(): void { global $nativeGlobal; @@ -22,6 +25,13 @@ function readGlobal(): int return $nativeGlobal->value; } +function replaceGlobalWithChild(): void +{ + global $nativeGlobal; + $nativeGlobal = new NativeCounterChild(); + $nativeGlobal->value = 41; +} + function nextStatic(): int { static $counter = new NativeCounter(); @@ -32,11 +42,14 @@ function main(): void { initializeGlobal(); var_dump(readGlobal()); + replaceGlobalWithChild(); + var_dump(readGlobal()); var_dump(nextStatic()); var_dump(nextStatic()); } ?> --EXPECT-- int(40) +int(41) int(1) int(2) diff --git a/tests/compiler/native-class/inherited-clone.phpt b/tests/compiler/native-class/inherited-clone.phpt new file mode 100644 index 00000000..f0c26d54 --- /dev/null +++ b/tests/compiler/native-class/inherited-clone.phpt @@ -0,0 +1,29 @@ +--TEST-- +Native class: clone resolves an inherited __clone method +--FILE-- +value++; + } +} + +#[Native] +class NativeCloneChild extends NativeCloneParent {} + +function main(): void +{ + $first = new NativeCloneChild(); + $second = clone $first; + var_dump($first->value, $second->value); +} +?> +--EXPECT-- +int(1) +int(2) diff --git a/tests/compiler/native-class/instanceof.phpt b/tests/compiler/native-class/instanceof.phpt index 5a525052..41169b49 100644 --- a/tests/compiler/native-class/instanceof.phpt +++ b/tests/compiler/native-class/instanceof.phpt @@ -31,6 +31,9 @@ function main(): void var_dump($object instanceof NativeInstanceofBase); var_dump($object instanceof NativeInstanceofOther); var_dump(makeInstanceofChild() instanceof NativeInstanceofChild); + $object = null; + var_dump($object instanceof NativeInstanceofChild); + var_dump($object instanceof NativeInstanceofBase); } ?> --EXPECT-- @@ -39,3 +42,5 @@ bool(true) bool(false) made bool(true) +bool(false) +bool(false) diff --git a/tests/compiler/native-class/interface-property-hooks.phpt b/tests/compiler/native-class/interface-property-hooks.phpt new file mode 100644 index 00000000..d562d4af --- /dev/null +++ b/tests/compiler/native-class/interface-property-hooks.phpt @@ -0,0 +1,44 @@ +--TEST-- +Native class: interface property hook contracts remain compile-time only +--FILE-- + strtoupper($this->stored); + set => $this->stored = trim($value); + } +} + +function main(): void +{ + $value = new NativeHookedName(); + echo $value->name, "\n"; + $value->name = ' changed '; + echo $value->name, "\n"; + + // The interface remains registered for ordinary PHP code, while the + // Native implementation exists only in the compile-time class graph. + var_dump(interface_exists(NativeMutableName::class)); + var_dump(class_exists(NativeHookedName::class)); +} +?> +--EXPECT-- +INITIAL +CHANGED +bool(true) +bool(false) diff --git a/tests/compiler/native-class/internal-interface.phpt b/tests/compiler/native-class/internal-interface.phpt index 61b3f77f..1252e727 100644 --- a/tests/compiler/native-class/internal-interface.phpt +++ b/tests/compiler/native-class/internal-interface.phpt @@ -16,9 +16,11 @@ function main(): void { $value = new NativeCountableValue(); var_dump($value->count()); + var_dump(count($value)); var_dump($value instanceof Countable); } ?> --EXPECT-- int(3) +int(3) bool(true) diff --git a/tests/compiler/native-class/is-null.phpt b/tests/compiler/native-class/is-null.phpt new file mode 100644 index 00000000..8291f964 --- /dev/null +++ b/tests/compiler/native-class/is-null.phpt @@ -0,0 +1,22 @@ +--TEST-- +Native class: is_null checks the nullable pointer without ZendVM +--FILE-- + +--EXPECT-- +bool(true) +bool(false) diff --git a/tests/compiler/native-class/isset-empty.phpt b/tests/compiler/native-class/isset-empty.phpt new file mode 100644 index 00000000..2fdfcfe2 --- /dev/null +++ b/tests/compiler/native-class/isset-empty.phpt @@ -0,0 +1,50 @@ +--TEST-- +Native class: isset and empty inspect native pointer slots without ZendVM +--FILE-- +next)); + var_dump(empty($node->next)); + var_dump(isset($node->next->next)); + var_dump(empty($node->next->next)); + var_dump(isset($node->number)); + var_dump(empty($node->number)); + + $node->next = new NativeOptionalNode(); + var_dump(isset($node->next)); + var_dump(empty($node->next)); + var_dump(isset($node->next->next)); + var_dump(empty($node->next->next)); + + $node = null; + var_dump(isset($node)); + var_dump(empty($node)); +} +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) diff --git a/tests/compiler/native-class/keyword-conversions.phpt b/tests/compiler/native-class/keyword-conversions.phpt index b7ddcb9f..f031df45 100644 --- a/tests/compiler/native-class/keyword-conversions.phpt +++ b/tests/compiler/native-class/keyword-conversions.phpt @@ -51,6 +51,10 @@ function main(): void var_dump($value->toFloat()); var_dump($value->toBool()); var_dump($value->toString()); + var_dump((array) $value); + var_dump((int) $value); + var_dump((float) $value); + var_dump((bool) $value); var_dump((string) $value); var_dump(strval($value)); @@ -68,6 +72,13 @@ int(7) float(7.5) bool(true) string(7) "value=7" +array(1) { + [0]=> + int(7) +} +int(7) +float(7.5) +bool(true) string(7) "value=7" string(7) "value=7" string(5) "magic" diff --git a/tests/compiler/native-class/magic-methods.phpt b/tests/compiler/native-class/magic-methods.phpt index aadcc14e..136c2886 100644 --- a/tests/compiler/native-class/magic-methods.phpt +++ b/tests/compiler/native-class/magic-methods.phpt @@ -26,6 +26,7 @@ function main(): void var_dump((string) $value); var_dump('value=' . $value); var_dump($value(42)); + var_dump($value instanceof Stringable); } ?> --EXPECT-- @@ -33,3 +34,4 @@ native string(6) "native" string(12) "value=native" string(9) "native:42" +bool(true) diff --git a/tests/compiler/native-class/non-null-parameter.phpt b/tests/compiler/native-class/non-null-parameter.phpt index 6e399bf2..b9c21b79 100644 --- a/tests/compiler/native-class/non-null-parameter.phpt +++ b/tests/compiler/native-class/non-null-parameter.phpt @@ -6,6 +6,7 @@ Native class: non-null parameters are validated at function entry #[Native] class NativeRequiredArgument { + public int $value = 42; } function acceptRequiredNative(NativeRequiredArgument $value): void @@ -13,9 +14,26 @@ function acceptRequiredNative(NativeRequiredArgument $value): void echo "entered\n"; } +function readAfterPossibleRebind(NativeRequiredArgument $value, bool $clear): int +{ + $before = $value->value; + if ($clear) { + $value = null; + } + // The assignment above conservatively invalidates the entry proof, so + // this access must check the pointer even on a control-flow merge. + return $before + $value->value; +} + function main(): void { $value = new NativeRequiredArgument(); + var_dump(readAfterPossibleRebind($value, false)); + try { + readAfterPossibleRebind($value, true); + } catch (Error $error) { + echo "rebound rejected\n"; + } unset($value); try { acceptRequiredNative($value); @@ -25,4 +43,6 @@ function main(): void } ?> --EXPECT-- +int(84) +rebound rejected rejected diff --git a/tests/compiler/native-class/nullable-signatures.phpt b/tests/compiler/native-class/nullable-signatures.phpt index 5b4b54b1..98dcc993 100644 --- a/tests/compiler/native-class/nullable-signatures.phpt +++ b/tests/compiler/native-class/nullable-signatures.phpt @@ -25,26 +25,12 @@ function readMaybeNative(?NativeNullableValue $value): int return $value->value; } -function maybeNativeUnion(bool $create): NativeNullableValue|null -{ - return $create ? new NativeNullableValue() : null; -} - -function readMaybeNativeUnion(NativeNullableValue|null $value): int -{ - return $value === null ? -2 : $value->value; -} - function main(): void { var_dump(readMaybeNative(maybeNative(false))); var_dump(readMaybeNative(maybeNative(true))); - var_dump(readMaybeNativeUnion(maybeNativeUnion(false))); - var_dump(readMaybeNativeUnion(maybeNativeUnion(true))); } ?> --EXPECT-- int(-1) int(42) -int(-2) -int(42) diff --git a/tests/compiler/native-class/nullsafe.phpt b/tests/compiler/native-class/nullsafe.phpt new file mode 100644 index 00000000..60ee32d5 --- /dev/null +++ b/tests/compiler/native-class/nullsafe.phpt @@ -0,0 +1,56 @@ +--TEST-- +Native class: nullsafe chains remain in the native pointer model +--FILE-- +number = $number; + } + + public function value(int $offset = 0): int + { + return $this->number + $offset; + } + + public function child(): ?NativeNullsafeNode + { + return $this->next; + } +} + +function offsetValue(): int +{ + echo "offset\n"; + return 2; +} + +function main(): void +{ + $node = new NativeNullsafeNode(40); + $node->next = new NativeNullsafeNode(10); + var_dump($node?->value(offsetValue())); + var_dump($node?->next?->value()); + var_dump($node?->child()?->number); + + $node = null; + var_dump($node?->value(offsetValue())); + var_dump($node?->next?->value()); + $child = $node?->child(); + var_dump(isset($child)); +} +?> +--EXPECT-- +offset +int(42) +int(10) +int(10) +NULL +NULL +bool(false) diff --git a/tests/compiler/native-class/numeric-properties.phpt b/tests/compiler/native-class/numeric-properties.phpt new file mode 100644 index 00000000..705cb252 --- /dev/null +++ b/tests/compiler/native-class/numeric-properties.phpt @@ -0,0 +1,42 @@ +--TEST-- +Native class: high precision numeric properties retain their PHPX value semantics +--FILE-- +integer = std::bigInt('123456789012345678901234567890'); + $this->floating = std::bigFloat('3.141592653589793238462643383279'); + $this->decimal = std::decimal('199.9500'); + } +} + +function main(): void +{ + $value = new NativeNumericProperties(); + $value->initialize(); + $alias = $value; + + echo $alias->integer->toString(), "\n"; + echo $alias->floating->toString(), "\n"; + echo $alias->decimal->toString(), "\n"; + + $copy = clone $value; + $copy->integer += 10; + echo $value->integer->toString(), "\n"; + echo $copy->integer->toString(), "\n"; +} +?> +--EXPECT-- +123456789012345678901234567890 +3.141592653589793238462643383279 +199.9500 +123456789012345678901234567890 +123456789012345678901234567900 diff --git a/tests/compiler/native-class/parameter-semantics.phpt b/tests/compiler/native-class/parameter-semantics.phpt new file mode 100644 index 00000000..24e5f5c2 --- /dev/null +++ b/tests/compiler/native-class/parameter-semantics.phpt @@ -0,0 +1,53 @@ +--TEST-- +Native class: typed pointers share object identity without PHP references +--FILE-- +value = $value; + } +} + +function native_parameter_update(NativeParameterValue $value): void +{ + $value->value = 42; + // The pointer itself is passed by value. Rebinding this local slot must + // not clear or replace the caller's pointer. + $value = null; +} + +function native_parameter_nullable(?NativeParameterValue $value): ?NativeParameterValue +{ + return $value; +} + +function main(): void +{ + $value = new NativeParameterValue(1); + $alias = $value; + $alias->value = 2; + var_dump($value === $alias, $value->value); + native_parameter_update($value); + var_dump($value->value); + var_dump(native_parameter_nullable($value) === $value); + $nullable = native_parameter_nullable(null); + var_dump(isset($nullable)); + var_dump(function_exists('native_parameter_update')); + var_dump(function_exists('native_parameter_nullable')); +} + +?> +--EXPECT-- +bool(true) +int(2) +int(42) +bool(true) +bool(false) +bool(false) +bool(false) diff --git a/tests/compiler/native-class/polymorphic-clone.phpt b/tests/compiler/native-class/polymorphic-clone.phpt new file mode 100644 index 00000000..ca3b101f --- /dev/null +++ b/tests/compiler/native-class/polymorphic-clone.phpt @@ -0,0 +1,49 @@ +--TEST-- +Native class: cloning through a base pointer preserves the dynamic subclass +--FILE-- +baseValue; + } +} + +#[Native] +class NativePolymorphicCloneChild extends NativePolymorphicCloneBase +{ + public int $childValue = 10; + + public function __clone(): void + { + $this->baseValue++; + $this->childValue++; + } + + public function describe(): string + { + return 'child:' . $this->baseValue . ':' . $this->childValue; + } +} + +function cloneNativeBase(NativePolymorphicCloneBase $value): NativePolymorphicCloneBase +{ + return clone $value; +} + +function main(): void +{ + $source = new NativePolymorphicCloneChild(); + $copy = cloneNativeBase($source); + echo $source->describe(), "\n"; + echo $copy->describe(), "\n"; +} +?> +--EXPECT-- +child:1:10 +child:2:11 diff --git a/tests/compiler/native-class/return-nullability.phpt b/tests/compiler/native-class/return-nullability.phpt new file mode 100644 index 00000000..e2f53c7f --- /dev/null +++ b/tests/compiler/native-class/return-nullability.phpt @@ -0,0 +1,65 @@ +--TEST-- +Native class: return boundaries preserve nullable and non-null pointer contracts +--FILE-- +value); + + try { + requireNativeReturn(false); + } catch (Error $error) { + echo "null return rejected\n"; + } + + $reporting = error_reporting(); + var_dump(suppressedNativeReturn()->value, error_reporting() === $reporting); + + try { + missingNativeReturn(); + } catch (Error $error) { + echo "missing return rejected\n"; + } +} +?> +--EXPECT-- +bool(true) +int(42) +null return rejected +int(42) +bool(true) +missing return rejected diff --git a/tests/compiler/native-class/std-containers.phpt b/tests/compiler/native-class/std-containers.phpt new file mode 100644 index 00000000..ebb4bf15 --- /dev/null +++ b/tests/compiler/native-class/std-containers.phpt @@ -0,0 +1,70 @@ +--TEST-- +Native class: std containers store typed native object pointers safely +--FILE-- +value = $value; + } +} + +function main(): void +{ + $array = std::array(NativeContainerValue::class, 1); + $vector = std::vector(NativeContainerValue::class); + $map = std::map(Type::String, NativeContainerValue::class); + $ordered = std::ordered_map(Type::Int, NativeContainerValue::class); + + $array[0] = new NativeContainerValue(11); + $vector[] = new NativeContainerValue(22); + $map['value'] = new NativeContainerValue(33); + $ordered[4] = new NativeContainerValue(44); + + // Make the container slots the only roots, then allocate enough objects + // to force the Native heap to collect. + for ($i = 0; $i < 50000; $i++) { + $temporary = new NativeContainerValue($i); + } + $temporary = null; + + var_dump($array[0]->value); + var_dump($vector[0]->value); + var_dump($map['value']->value); + var_dump($ordered[4]->value); + + $total = 0; + foreach ($array as $value) { + $total += $value->value; + } + foreach ($vector as $value) { + $total += $value->value; + } + foreach ($map as $value) { + $total += $value->value; + } + foreach ($ordered as $value) { + $total += $value->value; + } + var_dump($total); + + $array[0] = null; + unset($vector[0]); + unset($map['value']); + unset($ordered[4]); + var_dump(isset($array[0])); +} + +?> +--EXPECT-- +int(11) +int(22) +int(33) +int(44) +int(110) +bool(false) diff --git a/tests/compiler/native-class/stream-property.phpt b/tests/compiler/native-class/stream-property.phpt new file mode 100644 index 00000000..b017c003 --- /dev/null +++ b/tests/compiler/native-class/stream-property.phpt @@ -0,0 +1,38 @@ +--TEST-- +Native class: Stream properties use fixed fields with runtime type validation +--FILE-- +stream = $value; +} + +function main(): void +{ + $object = new NativeStreamProperty(); + var_dump($object->stream); + + $stream = fopen('php://memory', 'w+'); + assignStreamFromMixed($object, $stream); + fwrite($object->stream, 'native stream'); + rewind($object->stream); + echo stream_get_contents($object->stream), "\n"; + + try { + assignStreamFromMixed($object, 42); + } catch (TypeError $error) { + echo "invalid stream rejected\n"; + } +} +?> +--EXPECT-- +NULL +native stream +invalid stream rejected diff --git a/tests/compiler/native-class/virtual-default-dispatch.phpt b/tests/compiler/native-class/virtual-default-dispatch.phpt new file mode 100644 index 00000000..53e52df3 --- /dev/null +++ b/tests/compiler/native-class/virtual-default-dispatch.phpt @@ -0,0 +1,46 @@ +--TEST-- +Native class: virtual calls use defaults from the dynamically selected implementation +--FILE-- +value()); + var_dump($value->value(5)); + var_dump($value->value(first: 6)); + var_dump($value->value(5, 50)); +} + +function main(): void +{ + throughBase(new NativeDefaultBase()); + throughBase(new NativeDefaultChild()); +} +?> +--EXPECT-- +int(11) +int(15) +int(16) +int(55) +int(22) +int(25) +int(26) +int(55) diff --git a/tests/compiler/native-class/virtual-signature-variance.phpt b/tests/compiler/native-class/virtual-signature-variance.phpt new file mode 100644 index 00000000..da68a441 --- /dev/null +++ b/tests/compiler/native-class/virtual-signature-variance.phpt @@ -0,0 +1,74 @@ +--TEST-- +Native class: virtual dispatch preserves PHP parameter contravariance and return covariance +--FILE-- +calls++; + return new NativeDog(); + } +} + +#[Native] +class NativeGrandDogTransformer extends NativeDogTransformer +{ + public function transform(NativeAnimal $value): NativeDog + { + $this->calls += 10; + return new NativeDog(); + } +} + +function transformThroughBase(NativeTransformer $transformer, NativeDog $value): NativeAnimal +{ + return $transformer->transform($value); +} + +function transformThroughChild(NativeDogTransformer $transformer, NativeAnimal $value): NativeDog +{ + return $transformer->transform($value); +} + +function main(): void +{ + $transformer = new NativeDogTransformer(); + $dog = new NativeDog(); + $animal = new NativeAnimal(); + $baseResult = transformThroughBase($transformer, $dog); + $childResult = transformThroughChild($transformer, $animal); + var_dump($transformer->calls); + var_dump($baseResult instanceof NativeAnimal); + var_dump($childResult instanceof NativeDog); + + $grand = new NativeGrandDogTransformer(); + transformThroughBase($grand, $dog); + transformThroughChild($grand, $animal); + var_dump($grand->calls); +} +?> +--EXPECT-- +int(2) +bool(true) +bool(true) +int(20) diff --git a/tests/compiler/native-class/zero-values.phpt b/tests/compiler/native-class/zero-values.phpt index 51a7c4b2..0cef316d 100644 --- a/tests/compiler/native-class/zero-values.phpt +++ b/tests/compiler/native-class/zero-values.phpt @@ -12,6 +12,8 @@ class NativeZeroValue public string $name; public array $items; public mixed $value; + public Stream $stream; + public stdClass $requiredObject; public ?stdClass $object; public ?NativeZeroValue $child; } @@ -26,6 +28,8 @@ function main(): void $value->name, $value->items, $value->value, + $value->stream, + $value->requiredObject, $value->object, $value->child === null, ); @@ -40,4 +44,6 @@ array(0) { } NULL NULL +NULL +NULL bool(true)