Native Class 支持 [第二阶段]

master
韩天峰 1 week ago
parent 981b8f54d7
commit d5f0a113e6
  1. 172
      docs/NATIVE_CLASS_OBJECT.md
  2. 51
      examples/native/class.php
  3. 12
      phpunit/code/native-class-any-escape.php
  4. 13
      phpunit/code/native-class-arithmetic-operator.php
  5. 4
      phpunit/code/native-class-attribute-enum.php
  6. 4
      phpunit/code/native-class-attribute-interface.php
  7. 4
      phpunit/code/native-class-attribute-trait.php
  8. 11
      phpunit/code/native-class-bare-return.php
  9. 16
      phpunit/code/native-class-count-without-countable.php
  10. 15
      phpunit/code/native-class-explicit-constructor-call.php
  11. 13
      phpunit/code/native-class-first-class-callable.php
  12. 11
      phpunit/code/native-class-function-first-class-callable.php
  13. 10
      phpunit/code/native-class-get-called-class.php
  14. 10
      phpunit/code/native-class-get-class-implicit.php
  15. 10
      phpunit/code/native-class-get-class.php
  16. 13
      phpunit/code/native-class-get-parent-class.php
  17. 20
      phpunit/code/native-class-global-type-change.php
  18. 6
      phpunit/code/native-class-implicit-nullable-parameter.php
  19. 11
      phpunit/code/native-class-late-static-constant.php
  20. 14
      phpunit/code/native-class-loose-equality.php
  21. 11
      phpunit/code/native-class-new-static.php
  22. 6
      phpunit/code/native-class-non-implementable-interface.php
  23. 11
      phpunit/code/native-class-null-return.php
  24. 6
      phpunit/code/native-class-null-union-parameter.php
  25. 10
      phpunit/code/native-class-object-cast.php
  26. 13
      phpunit/code/native-class-private-clone.php
  27. 18
      phpunit/code/native-class-property-hook-compound.php
  28. 18
      phpunit/code/native-class-property-hook-indirect-reference.php
  29. 18
      phpunit/code/native-class-property-hook-indirect-unset.php
  30. 18
      phpunit/code/native-class-property-hook-indirect-write.php
  31. 15
      phpunit/code/native-class-property-hook-isset.php
  32. 10
      phpunit/code/native-class-reference-assignment.php
  33. 12
      phpunit/code/native-class-reference-function.php
  34. 10
      phpunit/code/native-class-reference-method.php
  35. 6
      phpunit/code/native-class-reference-parameter.php
  36. 10
      phpunit/code/native-class-reference-return.php
  37. 12
      phpunit/code/native-class-std-container-argument.php
  38. 11
      phpunit/code/native-class-std-container-escape.php
  39. 11
      phpunit/code/native-class-untyped-parameter.php
  40. 6
      phpunit/code/native-class-variadic-parameter.php
  41. 19
      phpunit/code/native-class-virtual-byref-variance.php
  42. 25
      phpunit/code/native-class-virtual-named-gap.php
  43. 283
      phpunit/src/NativeClass/NativeClassValidationTest.php
  44. 252
      src/CompilerBase.php
  45. 20
      src/Context/CompilationStateTrait.php
  46. 13
      src/Context/FunctionContext.php
  47. 2
      src/Entity/FunctionDef.php
  48. 37
      src/Generator/CallArgumentGenerator.php
  49. 550
      src/NativeClass/NativeClassSupportTrait.php
  50. 83
      src/Optimizer/FuncCallOptimizer.php
  51. 45
      src/Parser/AssignOpTrait.php
  52. 12
      src/Parser/ClassConstantFetchTrait.php
  53. 39
      src/Parser/FunctionCallTrait.php
  54. 49
      src/Parser/MethodCallTrait.php
  55. 5
      src/Parser/NullsafeAccessTrait.php
  56. 48
      src/Parser/PropertyAccessTrait.php
  57. 72
      src/Parser/StdContainerTrait.php
  58. 1
      src/Parser/TypeConversionTrait.php
  59. 11
      src/Parser/UnaryExpressionTrait.php
  60. 15
      src/Preprocessor.php
  61. 23
      src/Translator.php
  62. 27
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  63. 47
      tests/compiler/native-class/abstract-method.phpt
  64. 30
      tests/compiler/native-class/by-reference.phpt
  65. 41
      tests/compiler/native-class/clone-expression.phpt
  66. 33
      tests/compiler/native-class/conditions.phpt
  67. 22
      tests/compiler/native-class/error-suppress-type.phpt
  68. 13
      tests/compiler/native-class/global-and-static.phpt
  69. 29
      tests/compiler/native-class/inherited-clone.phpt
  70. 5
      tests/compiler/native-class/instanceof.phpt
  71. 44
      tests/compiler/native-class/interface-property-hooks.phpt
  72. 2
      tests/compiler/native-class/internal-interface.phpt
  73. 22
      tests/compiler/native-class/is-null.phpt
  74. 50
      tests/compiler/native-class/isset-empty.phpt
  75. 11
      tests/compiler/native-class/keyword-conversions.phpt
  76. 2
      tests/compiler/native-class/magic-methods.phpt
  77. 20
      tests/compiler/native-class/non-null-parameter.phpt
  78. 14
      tests/compiler/native-class/nullable-signatures.phpt
  79. 56
      tests/compiler/native-class/nullsafe.phpt
  80. 42
      tests/compiler/native-class/numeric-properties.phpt
  81. 53
      tests/compiler/native-class/parameter-semantics.phpt
  82. 49
      tests/compiler/native-class/polymorphic-clone.phpt
  83. 65
      tests/compiler/native-class/return-nullability.phpt
  84. 70
      tests/compiler/native-class/std-containers.phpt
  85. 38
      tests/compiler/native-class/stream-property.phpt
  86. 46
      tests/compiler/native-class/virtual-default-dispatch.phpt
  87. 74
      tests/compiler/native-class/virtual-signature-variance.phpt
  88. 6
      tests/compiler/native-class/zero-values.phpt

@ -1,7 +1,7 @@
# Native Class Object 设计与实现 # Native Class Object 设计与实现
> 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、 > 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、
> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、单继承、有限虚分派和 > 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派和
> Interface 编译期契约已经落地;本文同时记录尚未开放的边界。 > Interface 编译期契约已经落地;本文同时记录尚未开放的边界。
## 1. 背景 ## 1. 背景
@ -70,6 +70,8 @@ class Child extends Base {}
``` ```
`#[Native]` 是 Native Class Object 的正式显式声明方式。未使用该注解的普通 class 继续进入现有 ZendVM Object 编译流程。 `#[Native]` 是 Native Class Object 的正式显式声明方式。未使用该注解的普通 class 继续进入现有 ZendVM Object 编译流程。
该注解只能用于具名 `class`;Interface、Trait 与 Enum 均不能声明为 Native。Trait 仍可由
Native Class 在 convert 阶段注入,但 Trait 自身不形成 Native runtime 类型。
## 5. 生成的 C++ 结构 ## 5. 生成的 C++ 结构
@ -123,14 +125,21 @@ Native 方法不生成 Zend method wrapper,也不注册到 ZendVM。普通 PHP
Native Class 使用 C++ public single inheritance 保持基类子对象布局。PHP 方法的实现主体仍然是 `php_*` 自由函数,不改为复杂的 C++ 成员函数模型。 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 ```cpp
struct php_app__base; struct php_app__base;
php::Str php_app__base__name(php_app__base &this_); php::Str php_app__base__name(php_app__base &this_);
struct php_app__base { struct php_app__base {
virtual php::Str __native_dispatch_name() { virtual php::Str __native_dispatch_base_name() {
return php_app__base__name(*this); return php_app__base__name(*this);
} }
@ -141,7 +150,7 @@ struct php_app__child;
php::Str php_app__child__name(php_app__child &this_); php::Str php_app__child__name(php_app__child &this_);
struct php_app__child : public php_app__base { 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); 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。 - private、static、constructor 和 destructor 不加入 virtual method family。
- PHP 不支持按参数签名重载;Native Class 同样不增加 C++ overload 语义。 - PHP 不支持按参数签名重载;Native Class 同样不增加 C++ overload 语义。
- override 必须通过现有 PHP 方法兼容性规则和 Interface 检查。 - 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 和已知精确类型完成去虚化。 这会提供继承所必需的有限单分派多态,但不支持变量方法名、运行时 overload resolution、`__call()` 或 ZendVM 动态调用。C++ 编译器仍可对 `final` class、`final` method 和已知精确类型完成去虚化。
@ -223,8 +241,9 @@ final class InvalidContext
| Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 | | Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 |
| `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 | | `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 |
| `mixed` | `php::Var` | 保存任意 PHP zval | | `mixed` | `php::Var` | 保存任意 PHP zval |
| union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | | 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 |
| BigInt/BigFloat/Decimal | 对应 PHPX 高精度类型 | 直接使用已有 RAII 类型 | | `?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 分派。 `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 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 循环引用 ### 7.1 Native Class 循环引用
两个或多个 Native Class 可以在属性类型上相互引用: 两个或多个 Native Class 可以在属性类型上相互引用:
@ -357,9 +386,9 @@ Native Class 属性始终保存指针,不按值嵌入另一个 Native struct
Native Heap tracing GC 能够遍历所有 Native 指针字段,因此 A 与 B 相互指向不会形成引用计数循环,也不会产生永久泄漏。裸指针字段本身没有析构动作。 Native Heap tracing GC 能够遍历所有 Native 指针字段,因此 A 与 B 相互指向不会形成引用计数循环,也不会产生永久泄漏。裸指针字段本身没有析构动作。
但如果循环中的每一条边都是 non-nullable,并且都要求在各自构造函数返回前完成初始化,就会形成无法构造的初始化死结:创建 A 需要 B,而创建 B 又需要 A。 Native Object 属性的零值是 `nullptr`,包括源码中声明为 non-nullable 的 Native Class
属性。non-nullable 约束只作用于后续显式赋值,不引入 PHP typed property 的 UNDEF
首版不引入“未初始化对象发布”或特殊的两阶段构造 API。循环对象图必须至少使用一条 nullable 边打破初始化环 状态。因此循环对象图可以先分别构造,再建立双向关系
```php ```php
$a = new A(); $a = new A();
@ -367,7 +396,7 @@ $b = new B($a);
$a->b = $b; $a->b = $b;
``` ```
如果编译器发现一个 Native Class 构造依赖环全部由必须在构造阶段赋值的 non-nullable 属性组成,应抛出 FatalError,并提示将至少一条边声明为 nullable。该检查针对构造初始化依赖,而不是简单禁止类型循环 不需要构造依赖 SCC 或“两阶段发布”机制;类型 SCC 仅用于 C++ 前置声明与生成顺序
## 8. 内存与生命周期 ## 8. 内存与生命周期
@ -377,7 +406,7 @@ Native Class Object 应使用独立的、非移动、精确 tracing GC。本文
### 8.1 Native Heap ### 8.1 Native Heap
Native Heap 使用 Arena/chunk/free-list 提供快速内存分配,但每个对象都具有位于 struct 之前的隐藏 GC header: 首版 Wren 派生实现为每个对象分配一块连续内存,并在 struct 前放置隐藏 GC header:
```cpp ```cpp
struct NativeGcHeader { struct NativeGcHeader {
@ -393,6 +422,9 @@ auto *point = native_heap.make<php_app__point>();
GC header 不属于生成的 C++ struct,也不会改变属性偏移。用户可见对象变量仍然只是一个 `native_struct *` GC header 不属于生成的 C++ struct,也不会改变属性偏移。用户可见对象变量仍然只是一个 `native_struct *`
当前分配器使用独立 non-moving allocation;Arena/chunk/free-list 可以作为后续分配器
优化,但不得改变对象地址稳定性、header 布局、精确 tracing 或 finalization 语义。
Native Heap 具有以下特征: Native Heap 具有以下特征:
- non-moving:对象地址从创建到回收始终不变。 - 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 深入扫描。 `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 管理 ### 8.3 Root 管理
@ -461,7 +493,6 @@ struct FunctionNativeRoots {
初版只在确定的 safe point 执行 GC: 初版只在确定的 safe point 执行 GC:
- Native Heap 分配量超过自适应阈值。 - Native Heap 分配量超过自适应阈值。
- Native 对象数量超过阈值。
- Request Shutdown 强制清理全部对象。 - Request Shutdown 强制清理全部对象。
Native GC 不暴露语言级显式收集函数。PHPX 内部的收集入口只供运行时阈值策略和底层测试使用,不注册为 TypePHP/PHP API。 Native GC 不暴露语言级显式收集函数。PHPX 内部的收集入口只供运行时阈值策略和底层测试使用,不注册为 TypePHP/PHP API。
@ -734,17 +765,16 @@ void php_move(php_app__point *point, php::Float x);
- 修改属性对调用者可见。 - 修改属性对调用者可见。
- 在函数内部重新赋值 `$point` 不影响调用者变量。 - 在函数内部重新赋值 `$point` 不影响调用者变量。
引用参数生成二级指针引用 这里不需要、也不允许 PHP 引用符号
```php ```php
function replace(Point &$point): void; function replace(Point &$point): void; // FatalError
$alias =& $point; // FatalError
refval($point); // FatalError
$point->toRef(); // FatalError
``` ```
近似生成: 普通的 `$alias = $point` 已经只复制有类型的指针,二者指向并修改同一个对象。
```cpp
void php_replace(php_app__point *&point);
```
返回 Native Object 时返回指针: 返回 Native Object 时返回指针:
@ -752,7 +782,12 @@ void php_replace(php_app__point *&point);
php_app__point *php_create_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 边界 ## 10. ZendVM 边界
@ -771,7 +806,10 @@ Native Class 的字段可以保存 `php::Var`、`php::Array` 或 `php::Object`
- 作为 `call_user_func()` 等动态 callback 的 receiver。 - 作为 `call_user_func()` 等动态 callback 的 receiver。
- 保存到 ZendVM 全局变量或对象属性中。 - 保存到 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。编译器不得静默装箱或降级,因为这会使性能模型不可预测。 任何跨越 ZendVM 边界的行为都应在编译期抛出 FatalError。编译器不得静默装箱或降级,因为这会使性能模型不可预测。
@ -879,6 +917,8 @@ ZendVM;普通 PHP class 实现该 Interface 的行为不变。Native Class 支
- 使用与 PHP 一致的规则检查 required method 和 hooked property 是否存在、visibility、 - 使用与 PHP 一致的规则检查 required method 和 hooked property 是否存在、visibility、
static/引用/variadic、参数与返回类型及属性读写约束是否兼容。 static/引用/variadic、参数与返回类型及属性读写约束是否兼容。
- 项目 Interface 使用预处理得到的完整声明;PHP 内置 Interface 使用 Reflection 得到的
正式签名。Tentative return type 保持 PHP 8.4 的非致命语义,不擅自升级为 FatalError。
- 在 Trait AST 注入及继承成员合并完成后检查,因此 Trait 或父类提供的方法可以满足 Interface。 - 在 Trait AST 注入及继承成员合并完成后检查,因此 Trait 或父类提供的方法可以满足 Interface。
- 支持 Interface 继承和多个 `implements` 声明。 - 支持 Interface 继承和多个 `implements` 声明。
- 不为 Native Class 生成 Interface vtable、runtime interface id、`zend_class_entry` 或 - 不为 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_app__user__set_name(*user, value);
``` ```
复合写入必须显式展开,并严格保持 PHP 从左到右的求值顺序 为保持 Native 分支简单且不存在隐式运行时分派,首版只支持直接读取和直接赋值
```php ```php
$user->count += getValue(); $value = $user->count;
``` $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);
``` ```
带 Hook 的属性禁止: 带 Hook 的属性禁止:
- `+=`、`.=` 等复合写入。
- `++`、`--`。
- `$object->hookedArray[] = ...`、元素赋值或元素 `unset()` 等间接写入。
- `isset()`、`empty()`。
- 取引用。 - 取引用。
- 引用返回。 - 引用返回。
- 返回底层属性 slot。 - 返回底层属性 slot。
@ -981,6 +1018,10 @@ Native Property Hook 只有编译期语义,不生成 Zend Property Hook 元数
`clone` 可以支持,但必须由编译器生成字段级浅复制,不能无条件依赖 C++ 默认 copy constructor。 `clone` 可以支持,但必须由编译器生成字段级浅复制,不能无条件依赖 C++ 默认 copy constructor。
当 receiver 的静态类型可能保存 Native 子类时,继承层次生成一个内部协变 virtual clone
thunk,由动态子类执行正确尺寸的字段复制并调用其 `__clone()`;不能按静态基类复制,否则会
发生 C++ object slicing。没有 Native 继承关系的类继续使用静态 clone 路径,不增加 vptr。
```php ```php
$copy = clone $source; $copy = clone $source;
``` ```
@ -1001,7 +1042,12 @@ php_app__user____clone(*copy);
- PHP Array 保持 PHP 的 copy-on-write 行为,不进行无条件深拷贝。 - PHP Array 保持 PHP 的 copy-on-write 行为,不进行无条件深拷贝。
- Zend Object 字段复制对象句柄,继续指向同一 Zend 对象。 - Zend Object 字段复制对象句柄,继续指向同一 Zend 对象。
- Native Object 字段复制指针,继续指向同一对象,保持浅复制语义。 - 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` 时编译期报错。 包含不可复制字段的 Native Class 必须显式禁止 clone;对它使用 `clone` 时编译期报错。
@ -1018,6 +1064,9 @@ php_app__user____construct(*object, args...);
构造函数抛出异常时,必须销毁已经初始化的字段,并从 Native Heap 活动对象集合中移除该对象。 构造函数抛出异常时,必须销毁已经初始化的字段,并从 Native Heap 活动对象集合中移除该对象。
与其他 TypePHP class 一致,`__construct()` 只能由 `new` 触发。显式调用
`$object->__construct()` 在编译期报错,避免重复初始化已经存活的 Native Object。
### 15.2 析构 ### 15.2 析构
`__destruct()` 与 PHP 的精确析构时机存在冲突。Tracing GC 只能保证在对象变为不可达并完成 GC 后执行资源清理,不能保证在最后一个变量离开作用域时立即执行。 `__destruct()` 与 PHP 的精确析构时机存在冲突。Tracing GC 只能保证在对象变为不可达并完成 GC 后执行资源清理,不能保证在最后一个变量离开作用域时立即执行。
@ -1049,6 +1098,10 @@ struct NativeTypeDescriptor {
5. 未复活对象调用 `destroy()`;实际 C++ destructor 只负责字段 RAII 和基类子对象清理,保持 `noexcept`。descriptor 已记录最派生类型,因此不依赖通过基类指针执行 `delete`,也不要求仅为销毁而给所有继承层次增加 vtable。 5. 未复活对象调用 `destroy()`;实际 C++ destructor 只负责字段 RAII 和基类子对象清理,保持 `noexcept`。descriptor 已记录最派生类型,因此不依赖通过基类指针执行 `delete`,也不要求仅为销毁而给所有继承层次增加 vtable。
6. finalizer 抛出异常时,GC 必须先恢复内部状态并保证对象最终可清理,再把异常传播到当前 TypePHP 异常边界;shutdown 阶段遵循单独的不可抛出策略。 6. finalizer 抛出异常时,GC 必须先恢复内部状态并保证对象最终可清理,再把异常传播到当前 TypePHP 异常边界;shutdown 阶段遵循单独的不可抛出策略。
Request shutdown 会在 finalization 前后各清空一次已注册的 global/static Native root。
这是必要的:`__destruct()` 可能在 finalization 中把 `$this` 重新写入某个全局槽,但 request
heap 随后仍会整体销毁;第二次清空可防止悬空指针进入下一 request。
这种设计保留 `__destruct()` 的资源清理能力,同时避免让复杂用户代码穿过 C++ destructor。它与 PHP 的主要差异是调用时机由 Native GC 决定,而不是引用计数降为零的时刻。 这种设计保留 `__destruct()` 的资源清理能力,同时避免让复杂用户代码穿过 C++ destructor。它与 PHP 的主要差异是调用时机由 Native GC 决定,而不是引用计数降为零的时刻。
### 15.3 `unset()` 与析构时机 ### 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()` `__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)` 在编译期直接报错。 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 | | 无类型属性 | 不支持,编译期 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()` | 支持 | | `__construct()` | 支持 |
| `clone` / `__clone()` | 支持 | | `clone` / `__clone()` | 支持 |
| Getter/Setter 注解 | 支持 | | Getter/Setter 注解 | 支持 |
| Property Hook | 支持 | | Property Hook | 支持直接 get/set;间接写入、复合写入、引用、isset/empty 不支持 |
| Trait AST 注入 | 支持,注入完成后按普通 Native member 编译 | | Trait AST 注入 | 支持,注入完成后按普通 Native member 编译 |
| `readonly` | 不支持,编译期 FatalError;PHP readonly 是依赖 Zend 属性初始化状态的运行时机制,与 Native 固定裸字段模型不兼容 | | `readonly` | 不支持,编译期 FatalError;PHP readonly 是依赖 Zend 属性初始化状态的运行时机制,与 Native 固定裸字段模型不兼容 |
| `toArray()`/`toInt()` 等关键词转换 | 支持,要求 Native Class 声明零参数且返回类型完全一致的方法 | | `toArray()`/`toInt()` 等关键词转换 | 支持,要求 Native Class 声明零参数且返回类型完全一致的方法 |
| `toString()` / `__toString()` | 支持确定 Native Call;字符串强转、`strval()`、拼接和 `echo` 使用同一规则 | | `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 | | `__invoke()` | 支持确定 Native Call |
| `__destruct()` | 支持,由 GC finalization 触发且每个对象最多一次 | | `__destruct()` | 支持,由 GC finalization 触发且每个对象最多一次 |
| Native Class 单继承 | 支持;与普通 ZendVM class 禁止互相继承 | | Native Class 单继承 | 支持;与普通 ZendVM class 禁止互相继承 |
| Native abstract class / abstract method | 支持;生成 pure virtual thunk,具体子类在编译期完成实现检查 |
| override method | 支持,继承链同名实例方法生成 virtual dispatch thunk | | override method | 支持,继承链同名实例方法生成 virtual dispatch thunk |
| 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 | | 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 |
| Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 | | Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 |
| `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 | | `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 |
| `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 |
| `==` / `!=`、大小及算术/位运算 | 不支持,编译期 FatalError;值相等应使用显式 Native 方法 |
| 动态属性 | 不支持 | | 动态属性 | 不支持 |
| `$nativeObject->$expr()` | 不支持,只允许命名方法调用 | | `$nativeObject->$expr()` | 不支持,只允许命名方法调用 |
| `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 | | `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 |
@ -1110,6 +1201,7 @@ $json = json_encode($nativeObject->toArray());
| `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 | | `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 |
| `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler | | `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler |
| Reflection | 不支持 | | Reflection | 不支持 |
| `get_class()` / `get_parent_class()` / `get_called_class()` | 不支持 Native runtime introspection;使用 `self::class`、`parent::class` 或具体类名 |
| WeakReference | 不支持 | | WeakReference | 不支持 |
| PHP serialize | 不支持 | | PHP serialize | 不支持 |
| PHP `json_encode()` | 不支持直接传入 Native Object;先显式调用 `toArray()` | | PHP `json_encode()` | 不支持直接传入 Native Object;先显式调用 `toArray()` |
@ -1117,10 +1209,14 @@ $json = json_encode($nativeObject->toArray());
| 动态 PHP/eval 使用 | 不支持 | | 动态 PHP/eval 使用 | 不支持 |
| 普通 PHP array 保存 Native Object | 不支持 | | 普通 PHP array 保存 Native Object | 不支持 |
| Box/Std Container 属性 | 不支持 | | 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 | | Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC |
| TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 | | 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. 编译器目录与隔离要求 ## 17. 编译器目录与隔离要求
@ -1216,7 +1312,7 @@ Native Class 的主要路径必须满足:
4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。 4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。
5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。 5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。
6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。 6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。
7. 支持 Property Hook 与复合写入 lowering 7. 支持 Property Hook 的直接 getter/setter lowering;复合写入等动态语义编译期拒绝
8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。 8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。
9. 最后评估 `json_encode()`、栈分配与逃逸分析。 9. 最后评估 `json_encode()`、栈分配与逃逸分析。

@ -0,0 +1,51 @@
<?php
#[Native]
#[Printer]
class Point
{
public int $x;
public int $y;
function foo()
{
var_dump($this->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";
}
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeAnyEscape
{
}
function main(): void
{
$value = new NativeAnyEscape();
$mixed = any($value);
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeArithmeticValue
{
public int $value = 1;
}
function main(): void
{
$value = new NativeArithmeticValue();
var_dump($value + 1);
}

@ -0,0 +1,4 @@
<?php
#[Native]
enum InvalidNativeEnum {}

@ -0,0 +1,4 @@
<?php
#[Native]
interface InvalidNativeInterface {}

@ -0,0 +1,4 @@
<?php
#[Native]
trait InvalidNativeTrait {}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeBareReturnValue
{
}
function invalidNativeBareReturn(): ?NativeBareReturnValue
{
return;
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeCounterWithoutContract
{
public function count(): int
{
return 1;
}
}
function main(): void
{
$value = new NativeCounterWithoutContract();
count($value);
}

@ -0,0 +1,15 @@
<?php
#[Native]
class NativeExplicitConstructorCall
{
public function __construct()
{
}
}
function main(): void
{
$value = new NativeExplicitConstructorCall();
$value->__construct();
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeFirstClassCallable
{
public function run(): void {}
}
function invalidNativeFirstClassCallable(): void
{
$object = new NativeFirstClassCallable();
$callback = $object->run(...);
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeFunctionFirstClassCallableValue {}
function nativeFunctionFirstClassCallableTarget(NativeFunctionFirstClassCallableValue $value): void {}
function invalidNativeFunctionFirstClassCallable(): void
{
$callback = nativeFunctionFirstClassCallableTarget(...);
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeGetCalledClass
{
public function name(): string
{
return get_called_class();
}
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeGetClassImplicit
{
public function name(): string
{
return get_class();
}
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeGetClass
{
public function name(): string
{
return get_class($this);
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeGetParentBase {}
#[Native]
class NativeGetParentChild extends NativeGetParentBase
{
public function parentName(): string|false
{
return get_parent_class($this);
}
}

@ -0,0 +1,20 @@
<?php
#[Native]
class NativeGlobalFirst {}
#[Native]
class NativeGlobalSecond {}
function setFirstNativeGlobal(): void
{
global $nativeGlobal;
$nativeGlobal = new NativeGlobalFirst();
}
function setSecondNativeGlobal(): void
{
global $nativeGlobal;
$nativeGlobal = new NativeGlobalSecond();
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeImplicitNullableParameter {}
function invalidNativeImplicitNullableParameter(NativeImplicitNullableParameter $value = null): void {}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeLateStaticConstant
{
public function className(): string
{
return static::class;
}
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeEqualityValue
{
public int $value = 1;
}
function main(): void
{
$left = new NativeEqualityValue();
$right = new NativeEqualityValue();
var_dump($left == $right);
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeLateStaticConstruction
{
public function duplicate(): NativeLateStaticConstruction
{
return new static();
}
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeThrowableContract implements Throwable
{
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeNullReturnValue
{
}
function invalidNativeNullReturn(): NativeNullReturnValue
{
return null;
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeNullUnionParameterValue {}
function invalidNativeNullUnionParameter(NativeNullUnionParameterValue|null $value): void {}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeObjectCastValue {}
function castNativeToObject(NativeObjectCastValue $value): object
{
return (object) $value;
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativePrivateClone
{
private function __clone(): void {}
}
function main(): void
{
$value = new NativePrivateClone();
$copy = clone $value;
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativePropertyHookCompound
{
private int $stored = 0;
public int $value {
get => $this->stored;
set => $this->stored = $value;
}
}
function native_property_hook_compound(): void
{
$object = new NativePropertyHookCompound();
$object->value += 1;
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativePropertyHookIndirectReference
{
private array $stored = [1];
public array $items {
get => $this->stored;
set => $this->stored = $value;
}
}
function invalidNativePropertyHookIndirectReference(): void
{
$object = new NativePropertyHookIndirectReference();
$reference =& $object->items[0];
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativePropertyHookIndirectUnset
{
private array $stored = [1];
public array $items {
get => $this->stored;
set => $this->stored = $value;
}
}
function invalidNativePropertyHookIndirectUnset(): void
{
$object = new NativePropertyHookIndirectUnset();
unset($object->items[0]);
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativePropertyHookIndirectWrite
{
private array $stored = [];
public array $items {
get => $this->stored;
set => $this->stored = $value;
}
}
function invalidNativePropertyHookIndirectWrite(): void
{
$object = new NativePropertyHookIndirectWrite();
$object->items[] = 1;
}

@ -0,0 +1,15 @@
<?php
#[Native]
class NativePropertyHookIsset
{
public int $value {
get => 1;
}
}
function native_property_hook_isset(): void
{
$object = new NativePropertyHookIsset();
isset($object->value);
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeReferenceAssignmentValue {}
function invalidNativeReferenceAssignment(): void
{
$value = new NativeReferenceAssignmentValue();
$alias =& $value;
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeReferenceFunctionValue {}
function acceptsReference(&$value): void {}
function invalidNativeReferenceFunction(): void
{
$value = new NativeReferenceFunctionValue();
acceptsReference(refval($value));
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeReferenceMethodValue {}
function invalidNativeReferenceMethod(): void
{
$value = new NativeReferenceMethodValue();
$value->toRef();
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeReferenceParameterValue {}
function invalidNativeReferenceParameter(NativeReferenceParameterValue &$value): void {}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeReferenceReturnValue {}
function &invalidNativeReferenceReturn(): NativeReferenceReturnValue
{
$value = new NativeReferenceReturnValue();
return $value;
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeStdContainerArgumentValue {}
function consume_native_std_container(mixed $value): void {}
function native_std_container_argument(): void
{
$vector = std::vector(NativeStdContainerArgumentValue::class);
consume_native_std_container($vector);
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeStdContainerEscapeValue {}
function native_std_container_escape(): void
{
$vector = std::vector(NativeStdContainerEscapeValue::class);
$vector[] = new NativeStdContainerEscapeValue();
$phpArray = $vector;
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeUntypedParameterValue {}
function acceptsUntypedNativeValue($value): void {}
function invalidNativeUntypedParameter(): void
{
acceptsUntypedNativeValue(new NativeUntypedParameterValue());
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeVariadicParameterValue {}
function invalidNativeVariadicParameter(NativeVariadicParameterValue ...$values): void {}

@ -0,0 +1,19 @@
<?php
#[Native]
class NativeByRefAnimal {}
#[Native]
class NativeByRefDog extends NativeByRefAnimal {}
#[Native]
abstract class NativeByRefTransformer
{
abstract public function transform(NativeByRefDog &$value): void;
}
#[Native]
class NativeByRefAnimalTransformer extends NativeByRefTransformer
{
public function transform(NativeByRefAnimal &$value): void {}
}

@ -0,0 +1,25 @@
<?php
#[Native]
class NativeNamedGapBase
{
public function value(int $first = 1, int $second = 10): int
{
return $first + $second;
}
}
#[Native]
class NativeNamedGapChild extends NativeNamedGapBase
{
public function value(int $first = 2, int $second = 20): int
{
return $first + $second;
}
}
function callNativeNamedGap(NativeNamedGapBase $value): int
{
return $value->value(second: 50);
}

@ -6,6 +6,27 @@ use TypePhp\Exception\TestError;
final class NativeClassValidationTest extends \BaseTest 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 public function testRejectsUntypedProperty(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -48,6 +69,55 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-std-container-property.php'); $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 public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -83,6 +153,20 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-closure-capture.php'); $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 public function testRejectsNativeObjectClosureParameter(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -107,10 +191,73 @@ final class NativeClassValidationTest extends \BaseTest
public function testRejectsUnsupportedNativeObjectUnion(): void public function testRejectsUnsupportedNativeObjectUnion(): void
{ {
$this->expectException(TestError::class); $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'); $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 public function testRejectsIncorrectNativeKeywordReturnType(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -243,4 +390,138 @@ final class NativeClassValidationTest extends \BaseTest
$this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`'); $this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`');
$this->compile('native-class-internal-interface-parameter.php'); $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');
}
} }

@ -708,6 +708,9 @@ class CompilerBase implements PropertyAccessContext
if ($expr->getLine() === $this->debugLine) { if ($expr->getLine() === $this->debugLine) {
dump($expr); dump($expr);
} }
if ($expr instanceof Node\Expr\BinaryOp) {
$this->assertNativeObjectBinaryOperatorSupported($expr);
}
switch ($type) { switch ($type) {
case 'Expr_Isset': case 'Expr_Isset':
return $this->parseIsset($expr); return $this->parseIsset($expr);
@ -1879,9 +1882,31 @@ class CompilerBase implements PropertyAccessContext
protected function detectClassOfExpr(NodeAbstract $expr): string 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_) { if ($expr instanceof Expr\Clone_) {
return $this->detectClassOfExpr($expr->expr); 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) { if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) {
return 'Closure'; return 'Closure';
} }
@ -1921,6 +1946,9 @@ class CompilerBase implements PropertyAccessContext
return $this->getFullClassName(); return $this->getFullClassName();
} }
if ($class === 'static') { if ($class === 'static') {
if ($this->classDef?->nativeObject) {
$this->fatalError($expr, 'Native classes do not support `new static()`');
}
// 无法在编译期获得 static 类的准确类名 // 无法在编译期获得 static 类的准确类名
return ''; return '';
} else { } else {
@ -2225,6 +2253,14 @@ class CompilerBase implements PropertyAccessContext
return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';'; return 'return ' . $this->parseChainedExpr($v->expr, self::OP_REFVAL) . ';';
} }
if ($v->expr === null) { 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')); $nullExpr = new Expr\ConstFetch(new Node\Name('null'));
if ($this->shouldCheckClosureReturnType()) { if ($this->shouldCheckClosureReturnType()) {
$this->checkCompositeTypeAssignment( $this->checkCompositeTypeAssignment(
@ -2348,7 +2384,31 @@ class CompilerBase implements PropertyAccessContext
"The return type is native object `{$nativeReturnClass}`, `{$objectClass}` given" "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); $expr = $this->parseExprAsValue($v->expr);
$returnType = $this->getReturnType(); $returnType = $this->getReturnType();
@ -2746,6 +2806,9 @@ class CompilerBase implements PropertyAccessContext
protected function detectTypeOfExpr($expr): string protected function detectTypeOfExpr($expr): string
{ {
if ($expr instanceof Expr\ErrorSuppress) {
return $this->detectTypeOfExpr($expr->expr);
}
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) { if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) {
$keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name)); $keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name));
if ($keywordType !== null) { if ($keywordType !== null) {
@ -3071,6 +3134,12 @@ class CompilerBase implements PropertyAccessContext
} }
$target = $this->preparePropertyWriteTarget($var); $target = $this->preparePropertyWriteTarget($var);
if ($this->isNativeObjectPropertyHook($var)) {
$this->fatalError(
$var,
'Native property hooks only support direct reads and assignments',
);
}
$getter = $this->getPropertyHookGetter($var); $getter = $this->getPropertyHookGetter($var);
$setter = $this->getPropertyHookSetter($var); $setter = $this->getPropertyHookSetter($var);
if ($getter !== null && $setter === null) { if ($getter !== null && $setter === null) {
@ -3111,6 +3180,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreInc(Expr\PreInc $expr): string protected function parsePreInc(Expr\PreInc $expr): string
{ {
$this->assertNotNullsafeWriteContext($expr->var); $this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, '+', true); $result = $this->genDynamicPropIncDec($expr->var, '+', true);
if ($result !== null) { if ($result !== null) {
return $result; return $result;
@ -3497,6 +3567,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
{ {
$this->assertNotNullsafeWriteContext($expr->var); $this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, $op, false); $result = $this->genDynamicPropIncDec($expr->var, $op, false);
if ($result !== null) { if ($result !== null) {
return $result; return $result;
@ -3545,6 +3616,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreDec(Expr\PreDec $expr): string protected function parsePreDec(Expr\PreDec $expr): string
{ {
$this->assertNotNullsafeWriteContext($expr->var); $this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, '-', true); $result = $this->genDynamicPropIncDec($expr->var, '-', true);
if ($result !== null) { if ($result !== null) {
return $result; return $result;
@ -3631,6 +3703,9 @@ class CompilerBase implements PropertyAccessContext
$className = $this->parseIdentifier($expr->class); $className = $this->parseIdentifier($expr->class);
if ($this->isNameExpr($expr->class)) { if ($this->isNameExpr($expr->class)) {
if ($className === 'static') { if ($className === 'static') {
if ($this->classDef?->nativeObject) {
$this->fatalError($expr, 'Native classes do not support `new static()`');
}
$cePtr = Symbol::getCalledCe(); $cePtr = Symbol::getCalledCe();
} else { } else {
if ($className === 'self') { if ($className === 'self') {
@ -3664,7 +3739,8 @@ class CompilerBase implements PropertyAccessContext
$this->fatalError($expr, "Native class `{$className}` does not have a constructor"); $this->fatalError($expr, "Native class `{$className}` does not have a constructor");
} }
return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor
. ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); })'; . ', [&](auto &this_) { '
. $this->getNativeObjectInitializerName($className) . '(this_); })';
} }
$nativeCtor = $this->getNativeMethod($expr, $className, '__construct'); $nativeCtor = $this->getNativeMethod($expr, $className, '__construct');
if ($nativeCtor === false) { if ($nativeCtor === false) {
@ -3674,7 +3750,8 @@ class CompilerBase implements PropertyAccessContext
? '' ? ''
: ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor); : ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor);
return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor
. ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); ' . ', [&](auto &this_) { '
. $this->getNativeObjectInitializerName($className) . '(this_); '
. self::PREFIX . $nativeCtor . '(this_' . $args . '); })'; . self::PREFIX . $nativeCtor . '(this_' . $args . '); })';
} }
$cePtr = $this->getClassEntryPtr($className); $cePtr = $this->getClassEntryPtr($className);
@ -3696,18 +3773,34 @@ class CompilerBase implements PropertyAccessContext
$this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand'); $this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand');
$class = $this->detectClassOfExpr($expr->expr); $class = $this->detectClassOfExpr($expr->expr);
if ($this->isNativeObjectClass($class)) { if ($this->isNativeObjectClass($class)) {
if (!$this->isVarExpr($expr->expr)) { $source = $this->isVarExpr($expr->expr)
$this->fatalError($expr, 'Native object clone currently requires a typed variable'); ? $this->parseIdentifier($expr->expr)
} : $this->materializeNativeObjectReceiver($expr->expr, $class);
$source = $this->parseIdentifier($expr->expr);
$cpp = $this->getNativeObjectCppName($class); $cpp = $this->getNativeObjectCppName($class);
$descriptor = $this->getNativeObjectDescriptorName($class); $descriptor = $this->getNativeObjectDescriptorName($class);
$classDef = $this->getClass($class);
$initializer = ''; $initializer = '';
if ($classDef->hasMethod('__clone')) { $cloneMethod = $this->findNativeObjectMethod($class, '__clone');
$clone = self::PREFIX . $this->getNativeName('__clone', $classDef->namespace, $classDef->name); 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_); '; $initializer = $clone . '(this_); ';
} }
if ($this->nativeObjectUsesVirtualClone($class)) {
return $this->getNativeObjectReceiver($source) . '.'
. self::NATIVE_VIRTUAL_CLONE_METHOD . '()';
}
return 'php::nativeClone<' . $cpp . '>(' . $descriptor . ', ' return 'php::nativeClone<' . $cpp . '>(' . $descriptor . ', '
. $this->getNativeObjectReceiver($source) . $this->getNativeObjectReceiver($source)
. ', [&](auto &this_) { ' . $initializer . '})'; . ', [&](auto &this_) { ' . $initializer . '})';
@ -3746,7 +3839,13 @@ class CompilerBase implements PropertyAccessContext
// a non-variable left operand. Native objects cannot cross into a // a non-variable left operand. Native objects cannot cross into a
// Variant, so sequence the original pointer expression directly. // Variant, so sequence the original pointer expression directly.
$value = $this->parseExprAsValue($expr->expr); $value = $this->parseExprAsValue($expr->expr);
return '(static_cast<void>(' . $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<void>(' . $value . '), false)';
} }
if ($this->isNameExpr($expr->class)) { if ($this->isNameExpr($expr->class)) {
@ -3875,7 +3974,7 @@ class CompilerBase implements PropertyAccessContext
if ($var->default) { if ($var->default) {
$class = $this->detectClassOfExpr($var->default); $class = $this->detectClassOfExpr($var->default);
if ($this->isNativeObjectClass($class)) { 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 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 未定义 // TypePHP 编译器不允许操作未定义的变量,PHP 的 isset($var) 可能 $var 未定义
$this->checkVarMustExist($node, $this->parseIdentifier($node)); $this->checkVarMustExist($node, $this->parseIdentifier($node));
$fn = $this->getChainedFunc($op); $fn = $this->getChainedFunc($op);
$expr = $node; $expr = $node;
if ($this->isVarExpr($expr)) { 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) { if (!$getValue) {
return $fn . '(' . $this->parseExpr($expr) . ')'; return $fn . '(' . $this->parseExpr($expr) . ')';
} }
@ -4060,6 +4176,13 @@ class CompilerBase implements PropertyAccessContext
if ($op === self::OP_REFVAL) { if ($op === self::OP_REFVAL) {
return $prop . '.toReference()'; 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 . ')'; 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 protected function parseCastArray(Expr\Cast\Array_ $expr): string
{ {
$this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand');
$native = $this->parseNativeObjectExplicitConversion($expr->expr, 'toArray');
if ($native !== null) {
return $native;
}
return $this->convertArrayExpr($this->parseExprAsValue($expr->expr)); return $this->convertArrayExpr($this->parseExprAsValue($expr->expr));
} }
@ -4144,6 +4351,10 @@ class CompilerBase implements PropertyAccessContext
protected function parseCastDouble(mixed $expr): string protected function parseCastDouble(mixed $expr): string
{ {
$this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand'); $this->assertExprCanBeUsedAsValue($expr->expr, 'cast operand');
$native = $this->parseNativeObjectExplicitConversion($expr->expr, 'toFloat');
if ($native !== null) {
return $native;
}
return $this->convertFloatExpr( return $this->convertFloatExpr(
$this->parseIdentifier($expr->expr), $this->parseIdentifier($expr->expr),
$this->detectTypeOfExpr($expr->expr) $this->detectTypeOfExpr($expr->expr)
@ -4581,9 +4792,11 @@ class CompilerBase implements PropertyAccessContext
if (isset($this->context->globalVars[$name])) { if (isset($this->context->globalVars[$name])) {
continue; continue;
} }
$stdContainerInfo = null;
$code .= $this->getIndent(); $code .= $this->getIndent();
if ($type === Type::STD_ARRAY) { if ($type === Type::STD_ARRAY) {
$info = $this->context->stdArrays[$name]; $info = $this->context->stdArrays[$name];
$stdContainerInfo = $info;
if (isset($info['boxExpr'])) { if (isset($info['boxExpr'])) {
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
} else { } else {
@ -4596,6 +4809,7 @@ class CompilerBase implements PropertyAccessContext
} }
} elseif ($type === Type::STD_VECTOR) { } elseif ($type === Type::STD_VECTOR) {
$info = $this->context->stdContainers[$name]; $info = $this->context->stdContainers[$name];
$stdContainerInfo = $info;
if (isset($info['boxExpr'])) { if (isset($info['boxExpr'])) {
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
} else { } else {
@ -4614,6 +4828,7 @@ class CompilerBase implements PropertyAccessContext
} }
} elseif ($type === Type::STD_MAP || $type === Type::STD_ORDERED_MAP) { } elseif ($type === Type::STD_MAP || $type === Type::STD_ORDERED_MAP) {
$info = $this->context->stdContainers[$name]; $info = $this->context->stdContainers[$name];
$stdContainerInfo = $info;
if (isset($info['boxExpr'])) { if (isset($info['boxExpr'])) {
$code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');'; $code .= 'auto &' . $name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' . $info['boxExpr'] . ', ' . $info['typeId'] . ');';
} else { } else {
@ -4633,6 +4848,14 @@ class CompilerBase implements PropertyAccessContext
$code .= ';'; $code .= ';';
} }
$code .= PHP_EOL; $code .= PHP_EOL;
if ($stdContainerInfo !== null
&& isset($stdContainerInfo['class'])
&& $this->isNativeObjectClass($stdContainerInfo['class'])
) {
$rootType = 'std::remove_reference_t<decltype(' . $name . '_ref)>';
$code .= $this->getIndent() . 'php::NativeContainerRootFrame<' . $rootType . '> '
. $name . '_native_root_frame(' . $name . '_ref);' . PHP_EOL;
}
} }
return $code; return $code;
} }
@ -4717,7 +4940,10 @@ class CompilerBase implements PropertyAccessContext
return ''; return '';
} }
if ($this->getNativeObjectReturnType($this->functionDef) !== null) { 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) { if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
return $this->genUnionCheckedReturn(self::VALUE_NULL); return $this->genUnionCheckedReturn(self::VALUE_NULL);

@ -9,6 +9,7 @@
namespace TypePhp\Context; namespace TypePhp\Context;
use PhpParser\Node\Expr\Variable; use PhpParser\Node\Expr\Variable;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef; use TypePhp\Entity\ClassDef;
use TypePhp\Entity\FunctionDef; use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\InterfaceDef; use TypePhp\Entity\InterfaceDef;
@ -74,7 +75,11 @@ trait CompilationStateTrait
$this->globalVars[$name] = $type; $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, '\\'); $class = ltrim($class, '\\');
if ($this->hasStaticVar($name)) { if ($this->hasStaticVar($name)) {
@ -86,6 +91,19 @@ trait CompilationStateTrait
} else { } else {
return; 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->globalVars[$slot] = $this->getNativeObjectPointerType($class);
$this->nativeGlobalObjects[$slot] = $class; $this->nativeGlobalObjects[$slot] = $class;
$this->addNativeObject($name, $class); $this->addNativeObject($name, $class);

@ -32,6 +32,16 @@ class FunctionContext
/** @var array<string, string> Native Object pointer variable => fully-qualified class name. */ /** @var array<string, string> Native Object pointer variable => fully-qualified class name. */
public array $nativeObjects = []; 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<string, true>
*/
public array $nonNullNativeObjects = [];
/** /**
* Declared object constraints that are not used for native-call dispatch. * Declared object constraints that are not used for native-call dispatch.
* *
@ -94,6 +104,7 @@ class FunctionContext
$this->arguments = []; $this->arguments = [];
$this->objects = []; $this->objects = [];
$this->nativeObjects = []; $this->nativeObjects = [];
$this->nonNullNativeObjects = [];
$this->declaredObjects = []; $this->declaredObjects = [];
$this->stdArrays = []; $this->stdArrays = [];
$this->stdContainers = []; $this->stdContainers = [];
@ -133,12 +144,14 @@ class FunctionContext
int $tmpVarIndex, int $tmpVarIndex,
array $declaredObjects, array $declaredObjects,
array $nativeObjects = [], array $nativeObjects = [],
array $nonNullNativeObjects = [],
): void ): void
{ {
$this->localVars = $localVars; $this->localVars = $localVars;
$this->tmpVarIndex = $tmpVarIndex; $this->tmpVarIndex = $tmpVarIndex;
$this->declaredObjects = $declaredObjects; $this->declaredObjects = $declaredObjects;
$this->nativeObjects = $nativeObjects; $this->nativeObjects = $nativeObjects;
$this->nonNullNativeObjects = $nonNullNativeObjects;
$this->beforeStmtLines = []; $this->beforeStmtLines = [];
$this->afterStmtLines = []; $this->afterStmtLines = [];
$this->objectProps = []; $this->objectProps = [];

@ -24,6 +24,8 @@ class FunctionDef
public string $params = ''; public string $params = '';
public string $namespace; public string $namespace;
public bool $method = false; 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. */ /** Fully-qualified declaring class for methods; empty for free functions. */
public string $declaringClass = ''; public string $declaringClass = '';
public bool $stub = false; public bool $stub = false;

@ -20,7 +20,12 @@ use TypePhp\Generator\Symbol;
trait CallArgumentGenerator 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); $functionDef = $this->getFunction($nativeFunc);
$providedArgs = []; $providedArgs = [];
@ -65,6 +70,12 @@ trait CallArgumentGenerator
// lowered in source order; sorting raw AST arguments here would also // lowered in source order; sorting raw AST arguments here would also
// reorder their side effects. // reorder their side effects.
if ($hasNamedArg) { if ($hasNamedArg) {
$lastProvidedIndex = $providedArgs === []
? $parameterOffset - 1
: max(array_keys($providedArgs));
if ($deferTrailingDefaults && $variadicArgCount > 0) {
$lastProvidedIndex = $variadicArgIndex;
}
// 命名参数中间存在空洞,需要使用默认参数填充 // 命名参数中间存在空洞,需要使用默认参数填充
foreach ($functionDef->argInfoList as $k => $argInfo) { foreach ($functionDef->argInfoList as $k => $argInfo) {
if ($k < $parameterOffset) { if ($k < $parameterOffset) {
@ -74,6 +85,20 @@ trait CallArgumentGenerator
continue; continue;
} }
if (!isset($providedArgs[$k])) { 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 === '') { if ($argInfo->default === '') {
$errorNode = null; $errorNode = null;
foreach ($callArgs as $a) { foreach ($callArgs as $a) {
@ -97,7 +122,7 @@ trait CallArgumentGenerator
if (count($sourceArgs) === 0 if (count($sourceArgs) === 0
and count($functionDef->argInfoList) === $parameterOffset + 1 and count($functionDef->argInfoList) === $parameterOffset + 1
and $functionDef->argInfoList[$parameterOffset]->variadic) { and $functionDef->argInfoList[$parameterOffset]->variadic) {
return '{}'; return $deferTrailingDefaults ? '' : '{}';
} }
$resolvedArgs = []; $resolvedArgs = [];
@ -606,6 +631,12 @@ trait CallArgumentGenerator
protected function parseCallArgValue(Node\Arg $arg): string protected function parseCallArgValue(Node\Arg $arg): string
{ {
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
if ($this->isVarExpr($arg->value)) {
$this->assertStdContainerDoesNotEscapeNativeObjects(
$arg,
$this->parseIdentifier($arg->value),
);
}
$class = $this->detectClassOfExpr($arg->value); $class = $this->detectClassOfExpr($arg->value);
if ($class !== '' && $this->isNativeObjectClass($class)) { if ($class !== '' && $this->isNativeObjectClass($class)) {
$this->fatalError( $this->fatalError(
@ -656,6 +687,8 @@ trait CallArgumentGenerator
$arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg); $arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg);
} }
$this->assertNativeObjectReferenceForbidden($arg->value, $arg);
if ($this->isVarExpr($arg->value)) { if ($this->isVarExpr($arg->value)) {
return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value)); return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value));
} }

@ -21,6 +21,8 @@ use PhpParser\Node;
trait NativeClassSupportTrait trait NativeClassSupportTrait
{ {
private const string NATIVE_VIRTUAL_CLONE_METHOD = '__typephp_native_clone';
/** /**
* Magic methods whose semantics require Zend object handlers, runtime * Magic methods whose semantics require Zend object handlers, runtime
* method resolution, dynamic properties, or Zend serialization state. * method resolution, dynamic properties, or Zend serialization state.
@ -41,6 +43,15 @@ trait NativeClassSupportTrait
'__debuginfo' => true, '__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 protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void
{ {
if (!$this->classDef?->nativeObject if (!$this->classDef?->nativeObject
@ -65,6 +76,12 @@ trait NativeClassSupportTrait
ClassDef $classDef, ClassDef $classDef,
string $interfaceName, string $interfaceName,
): void { ): 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); $interface = Reflection::getClass($interfaceName);
if ($interface === null) { if ($interface === null) {
$this->fatalError($node, "Internal interface `{$interfaceName}` is not available"); $this->fatalError($node, "Internal interface `{$interfaceName}` is not available");
@ -244,6 +261,60 @@ trait NativeClassSupportTrait
return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; 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 protected function getNativeObjectCppName(string|ClassDef $class): string
{ {
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\'));
@ -260,33 +331,76 @@ trait NativeClassSupportTrait
return $this->getNativeObjectCppName($class) . ' *'; 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 protected function getNativeObjectArgumentType(ArgInfo $argument): ?string
{ {
$class = $argument->declaredClass ?: $argument->class; $class = $argument->declaredClass ?: $argument->class;
if ((!$argument->byRef && $argument->type !== Type::OBJECT) if ($argument->type !== Type::OBJECT || !$this->isNativeObjectClass($class)) {
|| !$this->isNativeObjectClass($class)
) {
return null; return null;
} }
return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : ''); return $this->getNativeObjectPointerType($class);
} }
protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array /**
{ * A virtual adapter may widen value parameters, but C++ cannot safely
$inner = null; * adapt T*& to Base*&: the callee could replace it with a Base that is not
if ($type instanceof Node\NullableType) { * a T. Native objects intentionally carry no runtime class tag, so reject
$inner = $type->type; * that one PHP variance case at the declaration boundary.
} elseif ($type instanceof Node\UnionType && count($type->types) === 2) { */
foreach ($type->types as $member) { protected function assertNativeVirtualByRefStorageCompatible(
if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') { NodeAbstract $node,
continue; FunctionDef $child,
} FunctionDef $parent,
if ($inner !== null) { ): void {
return null; foreach ($parent->argInfoList as $index => $parentArgument) {
} if (!$parentArgument->byRef || !isset($child->argInfoList[$index])) {
$inner = $member; 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) { if (!$inner instanceof Node\Name) {
return null; return null;
} }
@ -365,10 +479,62 @@ trait NativeClassSupportTrait
return; return;
} }
$nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind); $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( $this->fatalError(
$errorNode, $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] ?? ''; 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 protected function getNativeObjectReceiver(string $name): string
{ {
if ($name === 'this_') { if ($name === 'this_') {
return 'this_'; return 'this_';
} }
if ($this->isNativeObjectKnownNonNull($name)) {
return '(*' . $name . ')';
}
$class = $this->getNativeObjectVarClass($name); $class = $this->getNativeObjectVarClass($name);
return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")';
} }
@ -467,6 +651,132 @@ trait NativeClassSupportTrait
return $object; 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 protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef
{ {
while ($class !== '' && $this->hasClass($class)) { while ($class !== '' && $this->hasClass($class)) {
@ -486,6 +796,11 @@ trait NativeClassSupportTrait
if ($classDef->hasMethod($method)) { if ($classDef->hasMethod($method)) {
return $classDef->getMethod($method); return $classDef->getMethod($method);
} }
if ($classDef->hasAbstractMethod($method)
&& isset($classDef->abstractMethodDefs[strtolower($method)])
) {
return $classDef->getAbstractMethod($method);
}
$class = $classDef->extends; $class = $classDef->extends;
} }
return null; return null;
@ -542,31 +857,74 @@ trait NativeClassSupportTrait
return $resolvedMethod; 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 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; return false;
} }
if ($method->flags & Modifiers::ABSTRACT) {
return true;
}
if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) { if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) {
return false; return false;
} }
if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) { if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) {
return true; return true;
} }
$parent = $class->extends; return false;
while ($parent !== '' && $this->hasClass($parent)) { }
$parentDef = $this->getClass($parent);
if ($parentDef->hasMethod($method->name)) { /**
return true; * 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<array{ClassDef, MethodDef}>
*/
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 protected function getNativeMethodReturnCppType(FunctionDef $function): string
@ -576,30 +934,65 @@ trait NativeClassSupportTrait
: ($this->getNativeObjectReturnType($function) ?? $function->returnType); : ($this->getNativeObjectReturnType($function) ?? $function->returnType);
} }
protected function getNativeMethodParameterDeclarations(FunctionDef $function): string protected function getNativeMethodParameterDeclarations(
FunctionDef $function,
?int $parameterCount = null,
): string
{ {
$args = []; $args = [];
foreach ($function->argInfoList as $argument) { $arguments = $parameterCount === null
? $function->argInfoList
: array_slice($function->argInfoList, 0, $parameterCount);
foreach ($arguments as $argument) {
if ($argument->variadic) { if ($argument->variadic) {
$args[] = Type::ARRAY . ' ' . $argument->name; $declaration = Type::ARRAY . ' ' . $argument->name;
} else { } else {
$args[] = $this->genArgumentDeclaration($argument); $declaration = $this->genArgumentDeclaration($argument);
} }
$args[] = $declaration;
} }
return implode(', ', $args); 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<int>
*/
protected function getNativeVirtualMethodArities(FunctionDef $function): array
{
$total = count($function->argInfoList);
return range(min($function->argCountRequired, $total), $total);
}
protected function getNativeObjectPropertyType(PropertyDef $property): string protected function getNativeObjectPropertyType(PropertyDef $property): string
{ {
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) {
return $this->getNativeObjectPointerType($property->class); return $this->getNativeObjectPointerType($property->class);
} }
return match ($property->type) { 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, 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_<class>__initialize and produced duplicate C++ definitions.
return 'typephp_native_initialize_fields__' . $this->getNativeObjectCppName($class);
}
protected function getNativeObjectPropertyCppName( protected function getNativeObjectPropertyCppName(
string|PropertyDef $property, string|PropertyDef $property,
string|ClassDef|null $declaringClass = null, string|ClassDef|null $declaringClass = null,
@ -740,20 +1133,35 @@ trait NativeClassSupportTrait
} }
$code .= ';' . PHP_EOL; $code .= ';' . PHP_EOL;
} }
foreach ($class->methods as $method) { foreach ([...$class->methods, ...$class->abstractMethodDefs] as $method) {
if (!$this->isNativeVirtualMethod($class, $method)) { foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) {
continue; $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 !== '' $code .= ';' . PHP_EOL;
&& $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 .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL;
$code .= 'extern const php::NativeTypeDescriptor ' $code .= 'extern const php::NativeTypeDescriptor '
. $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL; . $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL;
@ -766,9 +1174,11 @@ trait NativeClassSupportTrait
$cpp = $this->getNativeObjectCppName($class); $cpp = $this->getNativeObjectCppName($class);
$prefix = $cpp . '__gc'; $prefix = $cpp . '__gc';
$code = ''; $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)) { 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) { foreach ($class->properties as $property) {
if ($property->isStatic() || $property->default === null) { if ($property->isStatic() || $property->default === null) {
@ -783,21 +1193,45 @@ trait NativeClassSupportTrait
} }
$code .= '}' . PHP_EOL . PHP_EOL; $code .= '}' . PHP_EOL . PHP_EOL;
foreach ($class->methods as $method) { 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( $nativeFunction = self::PREFIX . $this->getNativeName(
$method->name, $method->name,
$class->namespace, $class->namespace,
$class->name, $class->name,
); );
$code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name) foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) {
. '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL; $slotFunction = $slotMethod->functionDef;
$call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; $returnType = $this->getNativeMethodReturnCppType($slotFunction);
$code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; 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 .= '}' . PHP_EOL . PHP_EOL;
} }
$code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL; $code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL;

@ -165,9 +165,9 @@ trait FuncCallOptimizer
'is_bool' => ['constFold' => self::FOLD_SSA_TYPE, 'constFoldExtra' => Type::BOOL], 'is_bool' => ['constFold' => self::FOLD_SSA_TYPE, 'constFoldExtra' => Type::BOOL],
// Custom handlers // Custom handlers
'is_null' => ['handler' => 'genIsNull'], 'is_null' => ['handler' => 'genIsNull', 'nativeReceiver' => true],
'get_class' => ['handler' => 'genGetClassOptimized'], 'get_class' => ['handler' => 'genGetClassOptimized', 'nativeReceiver' => true],
'get_parent_class' => ['handler' => 'genGetParentClass'], 'get_parent_class' => ['handler' => 'genGetParentClass', 'nativeReceiver' => true],
'function_exists' => ['handler' => 'genFunctionExistsOptimized'], 'function_exists' => ['handler' => 'genFunctionExistsOptimized'],
'func_get_arg' => ['handler' => 'genFuncGetArgOptimized'], 'func_get_arg' => ['handler' => 'genFuncGetArgOptimized'],
'func_get_args' => ['handler' => 'genFuncGetArgsOptimized'], 'func_get_args' => ['handler' => 'genFuncGetArgsOptimized'],
@ -177,7 +177,9 @@ trait FuncCallOptimizer
'array_keys' => ['handler' => 'genArrayKeys'], 'array_keys' => ['handler' => 'genArrayKeys'],
'array_key_exists' => ['handler' => 'genArrayKeyExists'], 'array_key_exists' => ['handler' => 'genArrayKeyExists'],
'round' => ['handler' => 'genRound'], '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'], 'define' => ['handler' => 'genDefine'],
'is_callable' => ['handler' => 'genIsCallable'], 'is_callable' => ['handler' => 'genIsCallable'],
]; ];
@ -216,10 +218,13 @@ trait FuncCallOptimizer
// language-level Native keyword aliases and are lowered to an exact // language-level Native keyword aliases and are lowered to an exact
// Native method; every other PHP function rejects Native pointers. // Native method; every other PHP function rejects Native pointers.
if (!isset($config['conversion'])) { if (!isset($config['conversion'])) {
foreach ($expr->args as $arg) { foreach ($expr->args as $index => $arg) {
if ($arg instanceof Node\Arg if ($arg instanceof Node\Arg
&& $this->isNativeObjectClass($this->detectClassOfExpr($arg->value)) && $this->isNativeObjectClass($this->detectClassOfExpr($arg->value))
) { ) {
if (($config['nativeReceiver'] ?? false) && $index === 0) {
continue;
}
$this->fatalError( $this->fatalError(
$arg, $arg,
'Native objects cannot cross a dynamic PHP/ZendVM call boundary', '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 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 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']); 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; $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)) { if ($this->isVarExpr($obj) && $this->isTypedObject($obj->name)) {
return $this->getLiteralString($this->getObjectType($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 protected function genGetParentClass(string $n, Node\Expr\FuncCall $e, array $c): string
{ {
if (count($e->args) === 0) { 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) { if ($this->classDef && $this->classDef->extends) {
return $this->getLiteralString($this->classDef->extends); return $this->getLiteralString($this->classDef->extends);
} }
return 'false'; return 'false';
} }
$arg = $e->args[0]->value; $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)) { if ($this->isScalarString($arg)) {
$cls = $this->getClass($arg->value); $cls = $this->getClass($arg->value);
if ($cls && $cls->extends) return $this->getLiteralString($cls->extends); 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 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); $folded = $this->doFoldCountLiteral($e);
if ($folded !== false) return $folded; if ($folded !== false) return $folded;
if (count($e->args) >= 2) { if (count($e->args) >= 2) {
@ -833,8 +892,14 @@ trait FuncCallOptimizer
$funcName = $expr->args[0]->value; $funcName = $expr->args[0]->value;
if ($this->isScalarString($funcName)) { if ($this->isScalarString($funcName)) {
$nameLower = strtolower(trim($funcName->value, '\\')); $nameLower = strtolower(trim($funcName->value, '\\'));
if ($this->findNativeFunction($nameLower)) { $nativeFunction = $this->findNativeFunction($nameLower);
return 'true'; 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); $funcName = $this->getLiteralString($nameLower);
return 'php::fn::function_exists(' . $funcName . ')'; return 'php::fn::function_exists(' . $funcName . ')';

@ -238,7 +238,7 @@ trait AssignOpTrait
|| $this->hasScopeGlobalVar($leftName) || $this->hasScopeGlobalVar($leftName)
|| $this->hasStaticVar($leftName); || $this->hasStaticVar($leftName);
if ($allowed && ($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 } elseif ($left instanceof Expr\PropertyFetch
&& $this->isVarExpr($left->var) && $this->isVarExpr($left->var)
@ -263,6 +263,15 @@ trait AssignOpTrait
&& $property->type === Type::OBJECT && $property->type === Type::OBJECT
&& $this->isNativeObjectClass($property->class); && $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) { if (!$allowed) {
$this->fatalError( $this->fatalError(
@ -350,6 +359,10 @@ trait AssignOpTrait
// 类型推断,获取对象的类名,如果不是对象则返回空字符串 // 类型推断,获取对象的类名,如果不是对象则返回空字符串
$rightClass = $this->detectClassOfExpr($right); $rightClass = $this->detectClassOfExpr($right);
if ($this->isNativeObjectVar($var)) { 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); $leftClass = $this->getNativeObjectVarClass($var);
if ($this->isNull($right)) { if ($this->isNull($right)) {
return $var . ' = nullptr'; return $var . ' = nullptr';
@ -436,6 +449,7 @@ trait AssignOpTrait
} }
} elseif ($this->isVarExpr($right)) { } elseif ($this->isVarExpr($right)) {
$rightVar = $this->parseIdentifier($right); $rightVar = $this->parseIdentifier($right);
$this->assertStdContainerDoesNotEscapeNativeObjects($right, $rightVar);
$type = $this->isStdContainer($rightVar) ? Type::ARRAY : $this->getVarType($rightVar); $type = $this->isStdContainer($rightVar) ? Type::ARRAY : $this->getVarType($rightVar);
$finalVarType = $this->getNormalAssignType($type); $finalVarType = $this->getNormalAssignType($type);
$leftClass = $this->getDeclaredObjectType($var); $leftClass = $this->getDeclaredObjectType($var);
@ -598,6 +612,7 @@ trait AssignOpTrait
protected function parseAssignOp(Expr\AssignOp $node, string $op): string protected function parseAssignOp(Expr\AssignOp $node, string $op): string
{ {
$this->assertNotNullsafeWriteContext($node->var); $this->assertNotNullsafeWriteContext($node->var);
$this->assertNativePropertyHookDirectWriteTarget($node->var);
$pythonOperator = $this->parsePythonAssignOperator($node); $pythonOperator = $this->parsePythonAssignOperator($node);
if ($pythonOperator !== null) { if ($pythonOperator !== null) {
return $pythonOperator; return $pythonOperator;
@ -605,6 +620,13 @@ trait AssignOpTrait
$propertyWriteTarget = $this->preparePropertyWriteTarget($node->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($node->var);
$this->guardLiteralDivisionByZero($node->expr, $op); $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)) { if ($node->var instanceof Expr\PropertyFetch && $this->isReadOnlyPropertyHook($node->var)) {
$this->fatalError($node->var, 'Cannot write to read-only hooked property'); $this->fatalError($node->var, 'Cannot write to read-only hooked property');
} }
@ -754,6 +776,21 @@ trait AssignOpTrait
} }
$rightType = $this->detectTypeOfExpr($node->expr); $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)) { if ($this->isFixedObjectProp($def) && $rightType !== Type::VAR && !$this->canAssignStaticTypeToObjectProperty($def, $rightType)) {
$this->fatalError( $this->fatalError(
$node->var, $node->var,
@ -906,10 +943,15 @@ trait AssignOpTrait
protected function parseAssignRef(Expr\AssignRef $expr): string protected function parseAssignRef(Expr\AssignRef $expr): string
{ {
$this->assertNotNullsafeWriteContext($expr->var); $this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->expr);
if ($expr->expr instanceof Expr\NullsafePropertyFetch) { if ($expr->expr instanceof Expr\NullsafePropertyFetch) {
$this->fatalError($expr->expr, 'Cannot take reference of a nullsafe chain'); $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 // A reference would outlive the constructor-only write window and
// make later mutations invisible to the compiler. It is therefore // make later mutations invisible to the compiler. It is therefore
// forbidden on either side even inside the declaring constructor. // forbidden on either side even inside the declaring constructor.
@ -1015,6 +1057,7 @@ trait AssignOpTrait
protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string protected function parseAssignPropertyArrayDim(NodeAbstract $left, NodeAbstract $right): string
{ {
$this->assertNativePropertyHookDirectWriteTarget($left);
$propertyWriteTarget = $this->preparePropertyWriteTarget($left->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($left->var);
$code = ''; $code = '';
$value = $this->parseExprAsValue($right); $value = $this->parseExprAsValue($right);

@ -50,6 +50,12 @@ trait ClassConstantFetchTrait
$const = $this->escapeString($this->parseIdentifier($expr->name)); $const = $this->escapeString($this->parseIdentifier($expr->name));
if ($class === 'static') { 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) { if (!$this->methodDef) {
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); $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); $class = $this->parseIdentifier($expr->class);
if ($class === 'static') { 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) { if (!$this->methodDef) {
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods");
} }

@ -99,6 +99,25 @@ trait FunctionCallTrait
} elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') {
$name = $this->parseIdentifier($expr->name); $name = $this->parseIdentifier($expr->name);
$globalName = ltrim($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)) { if ($this->isInternalFunction($globalName)) {
$this->assertWasiFunctionSupported($expr, $globalName); $this->assertWasiFunctionSupported($expr, $globalName);
$this->markInternalFunctionCallbackCall($globalName, $expr->args); $this->markInternalFunctionCallbackCall($globalName, $expr->args);
@ -110,7 +129,20 @@ trait FunctionCallTrait
if (count($expr->args) !== 1 || $expr->args[0]->unpack) { if (count($expr->args) !== 1 || $expr->args[0]->unpack) {
$this->fatalError($expr, 'The any function expects exactly one non-unpacked argument'); $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 ($globalName === 'expected' || $globalName === 'unexpected') {
if (count($expr->args) !== 1 || $expr->args[0]->unpack) { if (count($expr->args) !== 1 || $expr->args[0]->unpack) {
@ -125,6 +157,11 @@ trait FunctionCallTrait
$nativeFn = $this->findNativeFunction($name); $nativeFn = $this->findNativeFunction($name);
if ($nativeFn) { if ($nativeFn) {
$expr->setAttribute('nativeCall', $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])) { if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return $this->genPlaceHolder($this->identifierToStr($expr->name)); return $this->genPlaceHolder($this->identifierToStr($expr->name));

@ -406,14 +406,23 @@ trait MethodCallTrait
} }
if ($class !== '' && $this->isNativeObjectClass($class)) { 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)) { if (!$this->isNamedMethod($expr->name)) {
$this->fatalError($expr, 'Dynamic native object method calls are not supported'); $this->fatalError($expr, 'Dynamic native object method calls are not supported');
} }
$nativeMethodName = strtolower($expr->name->toString()); $nativeMethodName = strtolower($expr->name->toString());
if ($nativeMethodName === '__construct') {
$this->fatalError($expr, 'Explicit calls to native object constructors are not supported');
}
if ($nativeMethodName === '__destruct') { if ($nativeMethodName === '__destruct') {
$this->fatalError($expr, 'Explicit calls to native object destructors are not supported'); $this->fatalError($expr, 'Explicit calls to native object destructors are not supported');
} }
$nativeKeyword = $expr->name->toString(); $nativeKeyword = $expr->name->toString();
if ($nativeKeyword === 'toRef') {
$this->assertNativeObjectReferenceForbidden($expr->var, $expr);
}
if (isset(self::KEYWORD_METHOD_MAP[$nativeKeyword])) { if (isset(self::KEYWORD_METHOD_MAP[$nativeKeyword])) {
if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$nativeKeyword]) && $expr->args !== []) { if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$nativeKeyword]) && $expr->args !== []) {
$this->fatalError($expr, "The {$nativeKeyword} method does not accept parameters"); $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 // Native objects have their own C++ virtual thunk for an
// overridden family; do not let the Zend-object devirtualizer // overridden family; do not let the Zend-object devirtualizer
// downgrade this call to the dynamic path. // downgrade this call to the dynamic path.
$nativeMethodDef = $this->findNativeObjectMethod($class, $methodName);
$nativeFunc = $this->getNativeMethod($expr, $class, $methodName); $nativeFunc = $this->getNativeMethod($expr, $class, $methodName);
if ($nativeFunc === false) { 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); $expr->setAttribute('nativeCall', $nativeFunc);
$nativeFunctionDef = $this->getFunction($nativeFunc); $nativeFunctionDef = $this->getFunction($nativeFunc);
$declaringClass = $this->getClass($nativeFunctionDef->declaringClass); $declaringClass = $this->getClass($nativeFunctionDef->declaringClass);
$declaringMethod = $declaringClass->getMethod($methodName); $declaringMethod = $nativeMethodDef ?? $declaringClass->getMethod($methodName);
if ($this->isNativeVirtualMethod($declaringClass, $declaringMethod)) { if ($this->isNativeVirtualMethod($declaringClass, $declaringMethod)) {
$call = $this->getNativeObjectMemberReceiver($object) $call = $this->getNativeObjectMemberReceiver($object)
. $this->getNativeVirtualMethodName($methodName); . $this->getNativeVirtualMethodName($declaringClass, $methodName);
if ($expr->args === []) { if ($expr->args === []) {
return $call . '()'; return $call . '()';
} }
return $call . '(' . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; return $call . '(' . $this->parseNativeCallArgs(
$expr->args,
$nativeFunc,
deferTrailingDefaults: true,
) . ')';
} }
$receiver = $this->getNativeObjectReceiver($object); $receiver = $this->getNativeObjectReceiver($object);
if ($expr->args === []) { if ($expr->args === []) {
@ -756,6 +791,12 @@ trait MethodCallTrait
} }
$placeHolder = $fn; $placeHolder = $fn;
} elseif ($this->isNameExpr($expr->class) and $class === 'static') { } 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); $method = $this->parseIdentifier($expr->name);
$methodPtr = $this->identifierToStr($expr->name, literal: true); $methodPtr = $this->identifierToStr($expr->name, literal: true);
$fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')';

@ -38,6 +38,11 @@ trait NullsafeAccessTrait
Expr\PropertyFetch|Expr\MethodCall|Expr\NullsafePropertyFetch|Expr\NullsafeMethodCall $expr Expr\PropertyFetch|Expr\MethodCall|Expr\NullsafePropertyFetch|Expr\NullsafeMethodCall $expr
): string ): string
{ {
$native = $this->parseNativeNullsafeAccess($expr);
if ($native !== null) {
return $native;
}
$list = []; $list = [];
$ownedTmpVars = []; $ownedTmpVars = [];
$comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr])); $comment = $this->formatCppLineComment('Nullsafe Operator: ', $this->printer->prettyPrint([$expr]));

@ -415,6 +415,12 @@ trait PropertyAccessTrait
return $this->getLiteralString($this->classDef->extends); return $this->getLiteralString($this->classDef->extends);
} }
if ($name === 'static') { 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) { if (!$this->methodDef) {
$this->fatalError($class, "The 'static' keyword can only be used as the class name in class methods"); $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; return $rightExpr;
} }
if ($def->type === Type::STREAM) {
return $this->detectTypeOfExpr($right) === Type::STREAM
? $rightExpr
: 'php::toStream(' . $rightExpr . ')';
}
$typeCheck = $this->getObjectPropertyAssignTypeCheck($def); $typeCheck = $this->getObjectPropertyAssignTypeCheck($def);
if (empty($typeCheck)) { if (empty($typeCheck)) {
return $rightExpr; return $rightExpr;
@ -768,6 +780,7 @@ trait PropertyAccessTrait
$lines = []; $lines = [];
foreach ($vars as $var) { foreach ($vars as $var) {
$this->assertNotNullsafeWriteContext($var); $this->assertNotNullsafeWriteContext($var);
$this->assertNativePropertyHookDirectWriteTarget($var);
if ($this->isArrayDimFetch($var)) { if ($this->isArrayDimFetch($var)) {
if ($var->dim === null) { if ($var->dim === null) {
$this->fatalError($var, 'Cannot use [] for array unset'); $this->fatalError($var, 'Cannot use [] for array unset');
@ -835,6 +848,7 @@ trait PropertyAccessTrait
} }
$type = $this->getVarType($name); $type = $this->getVarType($name);
if ($this->isNativeObjectVar($name)) { if ($this->isNativeObjectVar($name)) {
$this->forgetNativeObjectNonNull($name);
$lines[] = "{$name} = nullptr;"; $lines[] = "{$name} = nullptr;";
} elseif ($this->isNativeType($type)) { } elseif ($this->isNativeType($type)) {
$this->warning($var, "Variable of native type `\${$name}` cannot be unset"); $this->warning($var, "Variable of native type `\${$name}` cannot be unset");
@ -957,6 +971,7 @@ trait PropertyAccessTrait
if ($this->isNativeObjectClass($nativeExpressionClass)) { if ($this->isNativeObjectClass($nativeExpressionClass)) {
$objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass); $objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass);
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR);
return $this->getNativeObjectMemberReceiver($objectName) return $this->getNativeObjectMemberReceiver($objectName)
. $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef);
} }
@ -976,6 +991,7 @@ trait PropertyAccessTrait
if ($resolution === null) { if ($resolution === null) {
$this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`"); $this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`");
} }
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_VAR);
return $this->getNativeObjectMemberReceiver($objectName) return $this->getNativeObjectMemberReceiver($objectName)
. $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef);
} }
@ -1040,6 +1056,38 @@ trait PropertyAccessTrait
return $this->getNativePropertyDef($expr)?->setter; 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 protected function isReadOnlyPropertyHook(NodeAbstract $expr): bool
{ {
if ($this->isPropertyHookBackingAccess($expr)) { if ($this->isPropertyHookBackingAccess($expr)) {

@ -95,6 +95,27 @@ trait StdContainerTrait
return $this->context->stdContainers[$var]; 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 protected function getStdContainerKeyType(string $var): string
{ {
if ($this->isStdVector($var) or $this->isStdArray($var)) { if ($this->isStdVector($var) or $this->isStdArray($var)) {
@ -107,7 +128,14 @@ trait StdContainerTrait
{ {
$info = $this->getStdContainerVarInfo($var); $info = $this->getStdContainerVarInfo($var);
if ($this->isStdArray($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']) { if ($info['type'] === Type::OBJECT and $info['class']) {
$this->addObject($valueVar, $info['class']); $this->addObject($valueVar, $info['class']);
@ -117,10 +145,10 @@ trait StdContainerTrait
return $info['type']; 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 = str_repeat(Type::STD_ARRAY . '<', count($sizes));
$decl .= $this->getStdContainerElementType($type); $decl .= $this->getStdContainerElementType($type, $class);
for ($i = count($sizes) - 1; $i >= 0; $i--) { for ($i = count($sizes) - 1; $i >= 0; $i--) {
$decl .= ', ' . $sizes[$i] . '>'; $decl .= ', ' . $sizes[$i] . '>';
} }
@ -146,7 +174,7 @@ trait StdContainerTrait
$nestedSizes = array_slice($sizes, $accessLevel); $nestedSizes = array_slice($sizes, $accessLevel);
return [ return [
'kind' => 'array', 'kind' => 'array',
'decl' => $this->getStdArrayDecl($info['type'], $nestedSizes), 'decl' => $this->getStdArrayDecl($info['type'], $nestedSizes, $info['class']),
'type' => $info['type'], 'type' => $info['type'],
'class' => $info['class'], 'class' => $info['class'],
'sizes' => array_reverse($nestedSizes), 'sizes' => array_reverse($nestedSizes),
@ -584,8 +612,11 @@ trait StdContainerTrait
return $container . '_ref.offsetUnset(' . $index . ')'; 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) { return match ($type) {
Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL, Type::STREAM, Type::BOX => Type::VAR, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL, Type::STREAM, Type::BOX => Type::VAR,
default => $type, default => $type,
@ -678,6 +709,20 @@ trait StdContainerTrait
} }
return $this->convertExprFromType($targetType, $valueExpr); 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); $rightClass = $this->detectClassOfExpr($expr);
if ($rightClass !== '') { if ($rightClass !== '') {
if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $class)) { if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $class)) {
@ -768,9 +813,15 @@ trait StdContainerTrait
$this->fatalError($expr, "{$owner} key only supports Type::Int or Type::String"); $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 protected function parseStdArray(string $var, Expr\StaticCall $expr): string
@ -807,7 +858,7 @@ trait StdContainerTrait
} }
$totalBytes = array_product($nesting) * $byte; $totalBytes = array_product($nesting) * $byte;
$decl = $this->getStdArrayDecl($type, $nesting); $decl = $this->getStdArrayDecl($type, $nesting, $typeInfo['class']);
$this->context->stdArrays[$var] = $this->addStdTypeId([ $this->context->stdArrays[$var] = $this->addStdTypeId([
'kind' => 'array', 'kind' => 'array',
'decl' => $decl, 'decl' => $decl,
@ -833,7 +884,8 @@ trait StdContainerTrait
} }
$size = $expr->args[1]->value->value; $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([ $this->context->stdContainers[$var] = $this->addStdTypeId([
'kind' => 'vector', 'kind' => 'vector',
'decl' => $decl, 'decl' => $decl,
@ -862,7 +914,7 @@ trait StdContainerTrait
$keyType = $this->parseStdMapKeyType($expr->args[0]->value, $funcName); $keyType = $this->parseStdMapKeyType($expr->args[0]->value, $funcName);
$valueTypeInfo = $this->parseStdValueTypeInfo($expr->args[1]->value, $funcName); $valueTypeInfo = $this->parseStdValueTypeInfo($expr->args[1]->value, $funcName);
$valueType = $valueTypeInfo['type']; $valueType = $valueTypeInfo['type'];
$decl = $this->getStdMapDecl($containerType, $keyType, $valueType); $decl = $this->getStdMapDecl($containerType, $keyType, $valueType, $valueTypeInfo['class']);
$this->context->stdContainers[$var] = $this->addStdTypeId([ $this->context->stdContainers[$var] = $this->addStdTypeId([
'kind' => $kind, 'kind' => $kind,
'decl' => $decl, 'decl' => $decl,

@ -264,6 +264,7 @@ trait TypeConversionTrait
protected function convertToRef(NodeAbstract $expr): string protected function convertToRef(NodeAbstract $expr): string
{ {
$this->assertNativeObjectReferenceForbidden($expr, $expr);
$this->checkLeftValue($expr); $this->checkLeftValue($expr);
$var = $this->parseIdentifier($expr); $var = $this->parseIdentifier($expr);
if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) { if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) {

@ -45,6 +45,10 @@ trait UnaryExpressionTrait
protected function parseCastInt(Expr\Cast\Int_ $node): string protected function parseCastInt(Expr\Cast\Int_ $node): string
{ {
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
$native = $this->parseNativeObjectExplicitConversion($node->expr, 'toInt');
if ($native !== null) {
return $native;
}
return $this->convertIntExpr( return $this->convertIntExpr(
$this->parseExprAsValue($node->expr), $this->parseExprAsValue($node->expr),
$this->detectTypeOfExpr($node->expr) $this->detectTypeOfExpr($node->expr)
@ -60,6 +64,10 @@ trait UnaryExpressionTrait
protected function parseCastBool(Expr\Cast\Bool_ $node): string protected function parseCastBool(Expr\Cast\Bool_ $node): string
{ {
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
$native = $this->parseNativeObjectExplicitConversion($node->expr, 'toBool');
if ($native !== null) {
return $native;
}
return $this->convertBoolExpr( return $this->convertBoolExpr(
$this->parseExprAsValue($node->expr), $this->parseExprAsValue($node->expr),
$this->detectTypeOfExpr($node->expr) $this->detectTypeOfExpr($node->expr)
@ -69,6 +77,9 @@ trait UnaryExpressionTrait
protected function parseCastObject(Expr\Cast\Object_ $node): string protected function parseCastObject(Expr\Cast\Object_ $node): string
{ {
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); $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)); return $this->convertObjectExpr($this->parseExprAsValue($node->expr));
} }

@ -432,9 +432,10 @@ class Preprocessor extends CompilerBase
// Record late-bound parameter type keywords so they can be re-resolved // 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. // to the consuming class when a trait method is flattened into a class.
$argInfo->typeKeyword = $typeKeyword; $argInfo->typeKeyword = $typeKeyword;
// Ordinary PHP references use php::Ref at the native ABI. Keep the // Ordinary PHP references use php::Ref at the native ABI. Native
// resolved declaration metadata above, however: Native object // object references are rejected after the complete signature has
// references need it to lower to `native_struct *&` instead. // been parsed: a typed pointer already shares object identity, while
// PHP & would additionally expose caller-slot rebinding.
return $param->byRef ? Type::REF : $type; return $param->byRef ? Type::REF : $type;
} }
@ -661,6 +662,7 @@ class Preprocessor extends CompilerBase
} }
$this->parseParams($v->params, $functionDef); $this->parseParams($v->params, $functionDef);
$this->assertNativeObjectFunctionSignature($v, $functionDef);
if ($this->classDef !== null if ($this->classDef !== null
&& !$this->classDef->nativeObject && !$this->classDef->nativeObject
@ -1589,8 +1591,11 @@ class Preprocessor extends CompilerBase
if (is_string($traitOrigin)) { if (is_string($traitOrigin)) {
$this->methodDef->traitOrigin = $traitOrigin; $this->methodDef->traitOrigin = $traitOrigin;
} }
$this->methodDef->functionDef = $this->parseFunctionDecl($v); // Keep abstract method metadata in the symbol repository as well.
$this->methodDef->functionDef->method = true; // 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); $this->checkRequiredArgNum($name, $this->methodDef, $v);
if ($this->method === '__construct') { if ($this->method === '__construct') {
foreach ($v->params as $param) { foreach ($v->params as $param) {

@ -1742,6 +1742,9 @@ CODE;
$code .= $this->genDefaultArgumentHelperDeclarations(); $code .= $this->genDefaultArgumentHelperDeclarations();
foreach ($this->symbols->functions() as $name => $func) { foreach ($this->symbols->functions() as $name => $func) {
if ($func->abstractMethod) {
continue;
}
$functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func); $functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func);
$list = []; $list = [];
if ($func->method) { if ($func->method) {
@ -3731,6 +3734,13 @@ CODE;
if (!$argInfo->variadic and $argInfo->declaredClass) { if (!$argInfo->variadic and $argInfo->declaredClass) {
$this->addObject($argInfo->name, $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) { if ($this->functionDef->generator) {
@ -3747,6 +3757,7 @@ CODE;
$oriTmpVarIndex = $this->context->tmpVarIndex; $oriTmpVarIndex = $this->context->tmpVarIndex;
$oriDeclaredObjects = $this->context->declaredObjects; $oriDeclaredObjects = $this->context->declaredObjects;
$oriNativeObjects = $this->context->nativeObjects; $oriNativeObjects = $this->context->nativeObjects;
$oriNonNullNativeObjects = $this->context->nonNullNativeObjects;
/** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ /** 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 = new SsaBuilder($v->stmts, $this->functionDef->argInfoList);
$ssaBuilder->build(); $ssaBuilder->build();
@ -3764,6 +3775,7 @@ CODE;
$oriTmpVarIndex, $oriTmpVarIndex,
$oriDeclaredObjects, $oriDeclaredObjects,
$oriNativeObjects, $oriNativeObjects,
$oriNonNullNativeObjects,
); );
} }
@ -3918,7 +3930,7 @@ CODE;
} }
} }
private function validateMethodOverrideSignature( protected function validateMethodOverrideSignature(
NodeAbstract $v, NodeAbstract $v,
string $methodName, string $methodName,
MethodDef $childMethodDef, MethodDef $childMethodDef,
@ -4003,6 +4015,13 @@ CODE;
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass); $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( 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; $current = $classDef;
while (true) { while (true) {

@ -64,6 +64,15 @@ trait NativeTypeCompatibilityTrait
return true; return true;
} }
$classDef = $this->getClass($class); $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) { while (true) {
if ($isInterface) { if ($isInterface) {
if ($classDef->implements and in_array($expected, $classDef->implements)) { if ($classDef->implements and in_array($expected, $classDef->implements)) {
@ -148,6 +157,12 @@ trait NativeTypeCompatibilityTrait
{ {
$type = $this->detectTypeOfExpr($arg->value); $type = $this->detectTypeOfExpr($arg->value);
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
if ($this->isVarExpr($arg->value)) {
$this->assertStdContainerDoesNotEscapeNativeObjects(
$arg,
$this->parseIdentifier($arg->value),
);
}
if (!empty($argInfo->typeCheck)) { if (!empty($argInfo->typeCheck)) {
$this->checkCompositeTypeAssignment( $this->checkCompositeTypeAssignment(
@ -189,16 +204,6 @@ trait NativeTypeCompatibilityTrait
"Argument `{$argName}` must be a native object of type `{$declaredClass}`" "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); $expr = $this->parseOrderedArg($arg);
return $this->materializeCallArgValue($arg->value, $expr); return $this->materializeCallArgValue($arg->value, $expr);
} }
@ -206,6 +211,7 @@ trait NativeTypeCompatibilityTrait
if ($argInfo->byRef) { if ($argInfo->byRef) {
if ($this->isReferenceWrapperCall($arg->value)) { if ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->assertNativeObjectReferenceForbidden($inner, $arg);
$this->assertReadonlyPropertyReferenceForbidden($inner, $arg, false); $this->assertReadonlyPropertyReferenceForbidden($inner, $arg, false);
if ($this->isVarExpr($inner)) { if ($this->isVarExpr($inner)) {
$arg->value = $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'); $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
} }
} else { } else {
$this->assertNativeObjectReferenceForbidden($arg->value, $arg);
$this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false); $this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false);
} }
if ($this->isVarExpr($arg->value)) { if ($this->isVarExpr($arg->value)) {

@ -0,0 +1,47 @@
--TEST--
Native class: abstract methods dispatch through the native virtual thunk
--FILE--
<?php
#[Native]
abstract class NativeAbstractValue
{
abstract public function value(): int;
abstract public function label(string $prefix = 'value'): string;
}
#[Native]
class NativeConcreteValue extends NativeAbstractValue
{
public function value(): int
{
return 42;
}
public function label(string $prefix = 'concrete'): string
{
return $prefix . '=' . $this->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"

@ -1,30 +0,0 @@
--TEST--
Native class: by-reference parameters replace the caller pointer
--FILE--
<?php
#[Native]
class NativeReferenceValue
{
public int $value;
public function __construct(int $value)
{
$this->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)

@ -0,0 +1,41 @@
--TEST--
Native class: clone accepts rooted native-producing expressions
--FILE--
<?php
#[Native]
class NativeCloneExpressionValue
{
public int $number = 40;
public function __clone(): void
{
$this->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)

@ -0,0 +1,33 @@
--TEST--
Native class: object pointers use PHP object truthiness in conditions
--FILE--
<?php
#[Native]
class NativeConditionValue {}
function maybeNative(bool $present): ?NativeConditionValue
{
return $present ? new NativeConditionValue() : null;
}
function main(): void
{
$present = maybeNative(true);
$missing = maybeNative(false);
var_dump((bool) ($present && true));
var_dump((bool) ($missing || false));
var_dump(!$present, !$missing);
if ($present) {
echo "present\n";
}
}
?>
--EXPECT--
bool(true)
bool(false)
bool(false)
bool(true)
present

@ -0,0 +1,22 @@
--TEST--
Native class: error suppression preserves typed pointer identity
--FILE--
<?php
#[Native]
class NativeSuppressedExpression
{
public int $value = 1;
}
function main(): void
{
$first = @new NativeSuppressedExpression();
$second = $first;
$second->value = 42;
var_dump($first === $second, $first->value);
}
?>
--EXPECT--
bool(true)
int(42)

@ -9,6 +9,9 @@ class NativeCounter
public int $value; public int $value;
} }
#[Native]
class NativeCounterChild extends NativeCounter {}
function initializeGlobal(): void function initializeGlobal(): void
{ {
global $nativeGlobal; global $nativeGlobal;
@ -22,6 +25,13 @@ function readGlobal(): int
return $nativeGlobal->value; return $nativeGlobal->value;
} }
function replaceGlobalWithChild(): void
{
global $nativeGlobal;
$nativeGlobal = new NativeCounterChild();
$nativeGlobal->value = 41;
}
function nextStatic(): int function nextStatic(): int
{ {
static $counter = new NativeCounter(); static $counter = new NativeCounter();
@ -32,11 +42,14 @@ function main(): void
{ {
initializeGlobal(); initializeGlobal();
var_dump(readGlobal()); var_dump(readGlobal());
replaceGlobalWithChild();
var_dump(readGlobal());
var_dump(nextStatic()); var_dump(nextStatic());
var_dump(nextStatic()); var_dump(nextStatic());
} }
?> ?>
--EXPECT-- --EXPECT--
int(40) int(40)
int(41)
int(1) int(1)
int(2) int(2)

@ -0,0 +1,29 @@
--TEST--
Native class: clone resolves an inherited __clone method
--FILE--
<?php
#[Native]
class NativeCloneParent
{
public int $value = 1;
public function __clone(): void
{
$this->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)

@ -31,6 +31,9 @@ function main(): void
var_dump($object instanceof NativeInstanceofBase); var_dump($object instanceof NativeInstanceofBase);
var_dump($object instanceof NativeInstanceofOther); var_dump($object instanceof NativeInstanceofOther);
var_dump(makeInstanceofChild() instanceof NativeInstanceofChild); var_dump(makeInstanceofChild() instanceof NativeInstanceofChild);
$object = null;
var_dump($object instanceof NativeInstanceofChild);
var_dump($object instanceof NativeInstanceofBase);
} }
?> ?>
--EXPECT-- --EXPECT--
@ -39,3 +42,5 @@ bool(true)
bool(false) bool(false)
made made
bool(true) bool(true)
bool(false)
bool(false)

@ -0,0 +1,44 @@
--TEST--
Native class: interface property hook contracts remain compile-time only
--FILE--
<?php
interface NativeReadableName
{
public string $name { get; }
}
interface NativeMutableName extends NativeReadableName
{
public string $name { get; set; }
}
#[Native]
class NativeHookedName implements NativeMutableName
{
private string $stored = 'initial';
public string $name {
get => 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)

@ -16,9 +16,11 @@ function main(): void
{ {
$value = new NativeCountableValue(); $value = new NativeCountableValue();
var_dump($value->count()); var_dump($value->count());
var_dump(count($value));
var_dump($value instanceof Countable); var_dump($value instanceof Countable);
} }
?> ?>
--EXPECT-- --EXPECT--
int(3) int(3)
int(3)
bool(true) bool(true)

@ -0,0 +1,22 @@
--TEST--
Native class: is_null checks the nullable pointer without ZendVM
--FILE--
<?php
#[Native]
class NativeNullableCheck {}
function maybeNativeNullableCheck(bool $create): ?NativeNullableCheck
{
return $create ? new NativeNullableCheck() : null;
}
function main(): void
{
var_dump(is_null(maybeNativeNullableCheck(false)));
var_dump(is_null(maybeNativeNullableCheck(true)));
}
?>
--EXPECT--
bool(true)
bool(false)

@ -0,0 +1,50 @@
--TEST--
Native class: isset and empty inspect native pointer slots without ZendVM
--FILE--
<?php
#[Native]
class NativeOptionalNode
{
public ?NativeOptionalNode $next;
public int $number = 0;
}
function main(): void
{
$node = new NativeOptionalNode();
var_dump(isset($node));
var_dump(empty($node));
var_dump(isset($node->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)

@ -51,6 +51,10 @@ function main(): void
var_dump($value->toFloat()); var_dump($value->toFloat());
var_dump($value->toBool()); var_dump($value->toBool());
var_dump($value->toString()); 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((string) $value);
var_dump(strval($value)); var_dump(strval($value));
@ -68,6 +72,13 @@ int(7)
float(7.5) float(7.5)
bool(true) bool(true)
string(7) "value=7" 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(7) "value=7" string(7) "value=7"
string(5) "magic" string(5) "magic"

@ -26,6 +26,7 @@ function main(): void
var_dump((string) $value); var_dump((string) $value);
var_dump('value=' . $value); var_dump('value=' . $value);
var_dump($value(42)); var_dump($value(42));
var_dump($value instanceof Stringable);
} }
?> ?>
--EXPECT-- --EXPECT--
@ -33,3 +34,4 @@ native
string(6) "native" string(6) "native"
string(12) "value=native" string(12) "value=native"
string(9) "native:42" string(9) "native:42"
bool(true)

@ -6,6 +6,7 @@ Native class: non-null parameters are validated at function entry
#[Native] #[Native]
class NativeRequiredArgument class NativeRequiredArgument
{ {
public int $value = 42;
} }
function acceptRequiredNative(NativeRequiredArgument $value): void function acceptRequiredNative(NativeRequiredArgument $value): void
@ -13,9 +14,26 @@ function acceptRequiredNative(NativeRequiredArgument $value): void
echo "entered\n"; 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 function main(): void
{ {
$value = new NativeRequiredArgument(); $value = new NativeRequiredArgument();
var_dump(readAfterPossibleRebind($value, false));
try {
readAfterPossibleRebind($value, true);
} catch (Error $error) {
echo "rebound rejected\n";
}
unset($value); unset($value);
try { try {
acceptRequiredNative($value); acceptRequiredNative($value);
@ -25,4 +43,6 @@ function main(): void
} }
?> ?>
--EXPECT-- --EXPECT--
int(84)
rebound rejected
rejected rejected

@ -25,26 +25,12 @@ function readMaybeNative(?NativeNullableValue $value): int
return $value->value; 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 function main(): void
{ {
var_dump(readMaybeNative(maybeNative(false))); var_dump(readMaybeNative(maybeNative(false)));
var_dump(readMaybeNative(maybeNative(true))); var_dump(readMaybeNative(maybeNative(true)));
var_dump(readMaybeNativeUnion(maybeNativeUnion(false)));
var_dump(readMaybeNativeUnion(maybeNativeUnion(true)));
} }
?> ?>
--EXPECT-- --EXPECT--
int(-1) int(-1)
int(42) int(42)
int(-2)
int(42)

@ -0,0 +1,56 @@
--TEST--
Native class: nullsafe chains remain in the native pointer model
--FILE--
<?php
#[Native]
class NativeNullsafeNode
{
public ?NativeNullsafeNode $next;
public int $number;
public function __construct(int $number)
{
$this->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)

@ -0,0 +1,42 @@
--TEST--
Native class: high precision numeric properties retain their PHPX value semantics
--FILE--
<?php
#[Native]
class NativeNumericProperties
{
public BigInt $integer;
public BigFloat $floating;
public Decimal $decimal;
public function initialize(): void
{
$this->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

@ -0,0 +1,53 @@
--TEST--
Native class: typed pointers share object identity without PHP references
--FILE--
<?php
#[Native]
class NativeParameterValue
{
public int $value;
public function __construct(int $value)
{
$this->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)

@ -0,0 +1,49 @@
--TEST--
Native class: cloning through a base pointer preserves the dynamic subclass
--FILE--
<?php
#[Native]
class NativePolymorphicCloneBase
{
public int $baseValue = 1;
public function describe(): string
{
return 'base:' . $this->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

@ -0,0 +1,65 @@
--TEST--
Native class: return boundaries preserve nullable and non-null pointer contracts
--FILE--
<?php
#[Native]
class NativeReturnBase
{
public int $value = 42;
}
#[Native]
class NativeReturnChild extends NativeReturnBase
{
}
function maybeNativeReturn(bool $create): ?NativeReturnChild
{
return $create ? new NativeReturnChild() : null;
}
function requireNativeReturn(bool $create): NativeReturnBase
{
// The nullable producer is allowed as an expression, but the declared
// non-null return boundary validates the resulting pointer exactly once.
return maybeNativeReturn($create);
}
function suppressedNativeReturn(): NativeReturnBase
{
return @maybeNativeReturn(true);
}
function missingNativeReturn(): NativeReturnBase
{
}
function main(): void
{
var_dump(maybeNativeReturn(false) === null);
var_dump(requireNativeReturn(true)->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

@ -0,0 +1,70 @@
--TEST--
Native class: std containers store typed native object pointers safely
--FILE--
<?php
#[Native]
class NativeContainerValue
{
public int $value;
public function __construct(int $value)
{
$this->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)

@ -0,0 +1,38 @@
--TEST--
Native class: Stream properties use fixed fields with runtime type validation
--FILE--
<?php
#[Native]
class NativeStreamProperty
{
public Stream $stream;
}
function assignStreamFromMixed(NativeStreamProperty $object, mixed $value): void
{
$object->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

@ -0,0 +1,46 @@
--TEST--
Native class: virtual calls use defaults from the dynamically selected implementation
--FILE--
<?php
#[Native]
class NativeDefaultBase
{
public function value(int $first = 1, int $second = 10): int
{
return $first + $second;
}
}
#[Native]
class NativeDefaultChild extends NativeDefaultBase
{
public function value(int $first = 2, int $second = 20): int
{
return $first + $second;
}
}
function throughBase(NativeDefaultBase $value): void
{
var_dump($value->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)

@ -0,0 +1,74 @@
--TEST--
Native class: virtual dispatch preserves PHP parameter contravariance and return covariance
--FILE--
<?php
#[Native]
class NativeAnimal {}
#[Native]
class NativeDog extends NativeAnimal {}
#[Native]
class NativeTransformer
{
public int $calls = 0;
public function transform(NativeDog $value): NativeAnimal
{
return $value;
}
}
#[Native]
class NativeDogTransformer extends NativeTransformer
{
public function transform(NativeAnimal $value): NativeDog
{
$this->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)

@ -12,6 +12,8 @@ class NativeZeroValue
public string $name; public string $name;
public array $items; public array $items;
public mixed $value; public mixed $value;
public Stream $stream;
public stdClass $requiredObject;
public ?stdClass $object; public ?stdClass $object;
public ?NativeZeroValue $child; public ?NativeZeroValue $child;
} }
@ -26,6 +28,8 @@ function main(): void
$value->name, $value->name,
$value->items, $value->items,
$value->value, $value->value,
$value->stream,
$value->requiredObject,
$value->object, $value->object,
$value->child === null, $value->child === null,
); );
@ -40,4 +44,6 @@ array(0) {
} }
NULL NULL
NULL NULL
NULL
NULL
bool(true) bool(true)

Loading…
Cancel
Save