perf(compiler): optimize array statement writes and class constant lookups

- Add optimized array write operations that preserve references and avoid temporaries
- Implement direct array append and item assignment for known array types
- Optimize compound assignment operations like += for known array slots
- Add compile-time expansion for $this::CONST class constant fetches
- Cache process-stable class entry pointers at function level to avoid repeated lookups
- Optimize string concatenation with two scalar operands using efficient overload
- Skip unnecessary type conversions for arguments that already have exact types
- Add comprehensive test coverage for optimized array operations and object creation
- Implement lazy initialization of request-scoped array default templates
- Generate efficient
master
韩天峰 5 days ago
parent 7418b6247e
commit cb1e12f11f
  1. 212
      docs/OBJECT_CREATION.md
  2. 1
      docs/README.md
  3. 56
      examples/bench.php
  4. 2
      examples/micro_bench.php
  5. 16
      phpunit/code/class-constant-codegen.php
  6. 34
      phpunit/code/hot-path-codegen.php
  7. 70
      phpunit/code/new-object-codegen.php
  8. 34
      phpunit/src/ClassConstantCodegenTest.php
  9. 54
      phpunit/src/HotPathCodegenTest.php
  10. 2
      phpunit/src/LocalVariableInitializerTest.php
  11. 4
      phpunit/src/NativePropertyTest.php
  12. 134
      phpunit/src/NewObjectCodegenTest.php
  13. 8
      phpunit/src/OperatorTest.php
  14. 2
      phpunit/src/StringConcatAssignTest.php
  15. 25
      src/CompilerBase.php
  16. 4
      src/Context/FunctionContext.php
  17. 4
      src/Entity/PropertyDef.php
  18. 40
      src/Optimizer/FuncCallOptimizer.php
  19. 70
      src/Parser/AssignOpTrait.php
  20. 50
      src/Parser/BinaryOpTrait.php
  21. 31
      src/Parser/ClassConstantFetchTrait.php
  22. 25
      src/Preprocessor.php
  23. 233
      src/Translator.php
  24. 21
      tests/compiler/class/new-known-class-entry-hoisted.phpt
  25. 35
      tests/compiler/class/this-class-constant-static-expansion.phpt
  26. 59
      tests/compiler/object_property/default-initialization-paths.phpt
  27. 47
      tests/compiler/object_property/runtime-default-custom-handlers.phpt
  28. 44
      tests/compiler/operator/array-statement-write-optimized.phpt

@ -0,0 +1,212 @@
# Zend Object 创建与属性默认值初始化
本文记录 TypePHP 生成的 Zend Class 在 MINIT 和对象创建阶段的初始化职责,重点说明何时需要自定义 `create_object`、其中允许执行哪些行为,以及对象创建热路径上的性能边界。
本文只讨论注册到 ZendVM 的普通 TypePHP Class。`#[Native]` Class 使用 Native Heap 与 GC,不走本文流程。
## 1. 两个初始化阶段必须分开
TypePHP Class 的属性初始化分为两个阶段:
1. `gen_stub.php` 在 MINIT 生成 `register_class_*()`,建立 `zend_class_entry`、属性元数据和默认属性表;
2. 只有默认属性表无法准确表达的值,才在每次创建对象时由自定义 `create_object` 补充。
这两个阶段不能重复执行相同的属性赋值。`register_class_*()` 已写入的值会由 Zend 的 `object_properties_init()` 复制到新对象;再次调用 `zend_update_property()` 不仅没有语义价值,还会进入属性名查找、类型检查、handler 分派和引用计数路径。
## 2. gen_stub.php 负责的默认值
以下值可以准确写入 Zend Class 的默认属性表:
| 源代码默认值 | 注册阶段表示 | 是否需要在 `create_object` 中再次写入 |
|---|---|---|
| `null` | `ZVAL_NULL` | 否 |
| `bool` | `ZVAL_TRUE/FALSE` | 否 |
| `int` | `ZVAL_LONG` | 否 |
| `float` | `ZVAL_DOUBLE` | 否 |
| `string` | 持久化 `zend_string` | 否 |
| 标量常量表达式 | 编译期求值后的标量 zval | 否 |
| `[]` | `ZVAL_EMPTY_ARRAY` | 否 |
| 没有显式默认值的 TypePHP typed property | TypePHP 规定的零值、空字符串、空数组、`null` 或 `UNDEF` | 否 |
例如:
```php
class Value
{
private const BASE = 20;
public int $id = self::BASE + 3;
public string $name = 'type' . 'php';
public array $items = [];
}
```
只要表达式能够在编译期安全求值,以上三个属性都应完全依赖 Zend Class 默认属性表。创建 `Value` 时不得再次调用 `zend_update_property()`
## 3. 默认值何时需要运行时补充
当前 `gen_stub.php` 不能在默认属性表中准确表示以下值。
### 3.1 非空数组
非空数组默认值当前在注册函数中使用 `ZVAL_EMPTY_ARRAY` 作为占位值。每个对象必须构造独立、语义正确的数组值:
```php
class Request
{
public array $options = ['timeout' => 10];
}
```
因此 `Request::$options` 需要在 `create_object` 中补充。多个对象仍遵守 PHP 数组的 copy-on-write 语义;修改一个对象的数组不得影响其他对象。
数组常量也遵守相同规则。若编译器只能确定它是数组、不能证明它为空,则保守地保留运行时初始化。
### 3.2 Enum case
Enum case 是对象,不是标量常量:
```php
enum State
{
case Ready;
}
class Task
{
public State $state = State::Ready;
}
```
类注册代码当前只能先生成占位值,`create_object` 再取得真正的 enum case 对象并写入属性。因此“只有非空数组才需要自定义 `create_object`”并不成立,enum case 是明确的第二类反例。
### 3.3 无法安全解析的常量表达式
若预处理阶段无法证明默认值可由 Zend 默认属性表准确表达,编译器必须保守地保留运行时初始化。优化只能删除已证明冗余的工作,不能根据表达式外形猜测其运行时类型。
## 4. handlers 与父类 allocator
### 4.1 Property Hook 与非对称 set 可见性不单独触发
PHP 8.4 Property Hook、`private(set)` 和 `protected(set)` 会安装 TypePHP 自定义 object handlers,但这本身不要求覆盖 `create_object`。Zend 8.4 的 `object_properties_init()` 直接复制 class default table,不调用 read/write handler;普通 `php_std_create_object` 已能正确设置最终 handlers。
只有该类同时含有非空数组、enum case 等运行时默认值时,才需要自定义创建流程。补充初始化必须绕过 setter;即使使用 `zend_std_write_property()`,PHP 8.4 也会根据 Hook 元数据调用 setter。当前生成代码因此使用编译期已知的 property offset,经 PHPX `Object::attr(offset)` 直接更新 backing slot。
### 4.2 父类自定义对象分配器
若父类来自 PHP 内置扩展,或祖先类拥有自定义对象存储布局,子类不能绕过父类的 allocator。当前类因运行时默认值确实需要自定义创建流程时,必须先调用保存的父类 `create_object`,再补充当前类的值。
TypePHP 父类已经安装自定义 allocator 时,普通子类通常直接继承它。只有子类自身也需要补充初始化时,才生成新的委派层。
## 5. 自定义 create_object 的执行流程
生成代码通过 `typephp_create_object_with_defaults()` 完成以下步骤:
1. 保存类最终的 `default_object_handlers`
2. 若必须尊重父类对象布局,调用保存的父类 allocator;否则执行 `zend_objects_new()``object_properties_init()`
3. 临时把新对象切换到 Zend 标准 object handlers,确保异常路径和其他对象操作处于可控状态;
4. 只执行标记为 `requiresRuntimeDefaultInit` 的属性初始化,并通过缓存的 declared-property offset 直接写 backing slot;
5. 每次写入后检查 Zend 异常;
6. 无论正常返回还是发生 C++ 异常,都恢复最终 handlers;
7. 返回已完整初始化的 `zend_object *`
初始化器是模板参数和编译期 lambda,不使用 `std::function`,也不会为 lambda 动态分配内存。`delegate_to_base` 是调用点确定的布尔值,优化构建中通常可被 C++ 编译器折叠。
以下行为不属于 `create_object`
- PHP `__construct()` 的函数体;
- static property 默认值初始化;它在 `php_app_init()` 中完成;
- 已由默认属性表表达的标量、`null` 和空数组赋值;
- clone 后重新应用默认值;clone 应复制源对象当前状态,而不是重新创建默认状态。
## 6. 已修复的主要性能问题
旧生成逻辑只要类中存在任意显式非 static 默认值,就安装自定义 `create_object`,并在每次创建对象时重新 update 所有默认属性。这会产生两层重复成本:
1. 只含 `public int $value = 0` 的普通类也绕过标准快速创建路径;
2. 一个类只要含有一个非空数组,其他标量属性也会被逐个重复 update。
当前规则已经调整为:
- 只有确实需要运行时补充的属性才使 `requireCtor` 生效;
- 已由 `gen_stub.php` 准确注册的属性不会出现在运行时初始化 block 中;
- 只有 Hook/非对称可见性而没有运行时默认值的类不再生成空的自定义 allocator;
- Hook 与运行时默认值同时存在时,使用固定 property offset 更新 backing slot,不调用 setter。
在 micro benchmark 中,仅包含标量属性的 `new Foo()` 已从约 `1.8s` 降至约 `0.78s`,与同环境 ZendPHP 扣除空循环后的约 `0.83s` 接近。该数字只用于记录优化量级,不是跨机器性能承诺。
## 7. 剩余性能成本与后续方向
### 7.1 非空数组应改为请求级模板与 copy-on-write
当前实现仍在每个对象中重新构建 `php::Array` 及所有元素,这是剩余的最大常见成本。不能把非空数组放进 internal class 的默认属性表,但这不等于必须为每个对象重新构建数组。
推荐生成一个请求级默认值模板:
1. 每个包含运行时数组默认值的类拥有一组 `THREAD_LOCAL php::Var` 模板和一个初始化状态;
2. 第一次创建该类对象时,通过 `UNEXPECTED(!initialized)` 惰性构建该类的全部模板;
3. 后续创建对象时只把模板 zval 复制到目标 backing slot,即增加一次数组引用计数;
4. 某个对象第一次修改该属性时,由 Zend/PHPX 的 `SEPARATE_ARRAY` 执行 copy-on-write;
5. 在 `php_app_clean()` 中释放模板并重置初始化状态,不能让 request allocator 分配的 HashTable 跨越 RSHUTDOWN。
以如下默认值为例:
```php
class Request
{
public array $options = [
'timeout' => 10,
'headers' => ['Accept' => 'application/json'],
];
}
```
若创建一万个对象但不修改 `$options`,数组及嵌套数组只构建一次;每个对象只持有共享 zval。若其中一个对象执行 `$request->options['timeout'] = 30`,只有该对象在写入时分离,其他对象和模板保持不变。嵌套数组也继续使用 Zend 原有的逐层 copy-on-write 规则。
PHP 属性默认数组不能包含引用,允许出现在常量表达式中的对象主要是不可变的 enum case,因此共享模板符合默认属性语义。实现后仍须用 PHPT 覆盖顶层写入、嵌套写入、`unset`、引用写入和动态 ZendVM 写入,确认所有路径都会正确分离。
不能简单地在 MINIT 构造持久化数组并传给 `zend_declare_typed_property()`。TypePHP 注册的是 `ZEND_INTERNAL_CLASS`,Zend 8.4 明确禁止 internal property 使用 refcounted default zval;`_object_properties_init()` 的 internal-class 快速路径也不会增加默认值引用计数。非空 array 与 enum object 都属于 refcounted value。
因此,在不改变“TypePHP Class 注册为 internal class”这一基础设计、也不修改 Zend ABI 的前提下,非空数组仍不能进入 class default table;但可以在表外维护请求级模板,使数组构造成本从“每个对象一次”降为“每个请求、每个默认值一次”。
模板建议按类惰性初始化,而不是在 RINIT 无条件构建全部模板:大型项目中很多类在一次请求内不会实例化。每个对象只增加一个高度可预测的初始化状态分支;第一次之后该分支稳定为 false。
第二阶段可以为完全由标量、字符串和嵌套字面量组成的数组生成模块生命周期的 persistent immutable template。它能进一步消除每个请求的一次构建,但需要正确处理 persistent HashTable、interned string、嵌套数组、MSHUTDOWN 和 ZTS,并把包含运行时常量或 enum case 的数组留在请求级路径。该方案侵入性和验证成本明显更高,不应作为第一阶段实现。
### 7.2 已改为固定属性槽写入
运行时补充的属性在编译期已经知道 class、属性名、offset 和类型。当前实现复用 persistent property-offset cache,并通过 `php::Object::attr(offset)` 更新槽位,已经省去每个对象上的属性名 hash 查询、通用 write handler 和 Property Hook setter。
这里仍会为 initializer 建立一个短生命周期 `php::Object` carrier,并读取 offset cache。后续若 profiling 证明它是热点,可以在 MINIT 后直接保存最终 offset,或在 PHPX 增加不取得对象所有权的初始化 helper。任何进一步优化都必须继续处理旧值析构、引用计数、父类 private slot、Hook backing slot 和异常安全,不能退回裸指针的无保护赋值。
### 7.3 Enum case 可提前绑定
Enum case 同样是 refcounted object,不能直接作为 internal class 默认 zval。可考虑在 MINIT 缓存稳定的 enum case 指针或 zval,再在每次创建对象时执行正确的引用计数复制,从而省去重复 class/case 查找;仍不能省略对象属性写入本身。
### 7.4 继承链上的多层 allocator
父类和子类都拥有运行时默认值时,创建流程会逐层委派并执行各自初始化,成本随相关继承层数增长。未来可以对完全由 TypePHP 控制、且没有特殊对象布局的继承链合并初始化计划;内置扩展父类仍必须调用其 allocator。
### 7.5 保守常量可能产生不必要的 allocator
无法在预处理阶段解析的常量会保守进入运行时路径。可以在符号准备完成后增加一次统一的常量默认值分类,减少“实际是标量,但早期无法证明”的自定义 allocator。该优化必须保留 enum case 和数组常量的区别。
### 7.6 自定义 handlers 的动态访问成本
TypePHP 当前为普通 Zend Class 安装属性 handlers,以支持 typed property 的 unset 语义、Property Hook 和非对称写可见性。安装发生在 MINIT,不等同于安装自定义 `create_object`;但动态属性读写仍可能进入 handler。已被编译器解析为固定槽位的 Native 属性访问不应因此退化。
## 8. 回归测试要求
修改该流程至少应覆盖:
- 标量、标量常量表达式和空数组不生成自定义 allocator;
- 非空数组生成 allocator,且两个对象的数组修改互不影响;
- enum case 默认值在对象创建后是真正的 enum object;
- 仅含 Property Hook 或非对称 set 可见性的类不生成空 allocator,且 Reflection、动态读写行为不退化;
- Property Hook/非对称属性与运行时默认值组合时不触发 setter;
- 父子类分别声明运行时默认值时,父类和子类属性都正确;
- 继承内置扩展类时不破坏其对象布局;
- 异常路径恢复 object handlers;
- 自举编译和完整 PHPUnit/PHPT 回归通过。
当前针对代码生成的核心断言位于 `NewObjectCodegenTest`,运行语义由 `default-initialization-paths.phpt`、`default-expressions-inheritance.phpt` 和 Property Hook 测试组覆盖。

@ -25,6 +25,7 @@
- [重建 PHPX WASM 静态库](PHPX_WASM_BUILD.md):增量重编 `libphpx.a`、数值依赖重建与完整 SDK 重建边界。 - [重建 PHPX WASM 静态库](PHPX_WASM_BUILD.md):增量重编 `libphpx.a`、数值依赖重建与完整 SDK 重建边界。
- [核心重构计划](REFACTORING_PLAN.md) - [核心重构计划](REFACTORING_PLAN.md)
- [作用域管理设计](SCOPE_MANAGEMENT.md):`CallableScope`、`UserCodeScopeGuard` 与 `FakeScopeGuard` 的职责和使用边界。 - [作用域管理设计](SCOPE_MANAGEMENT.md):`CallableScope`、`UserCodeScopeGuard` 与 `FakeScopeGuard` 的职责和使用边界。
- [Zend Object 创建与属性默认值初始化](OBJECT_CREATION.md):`gen_stub.php` 默认属性表、自定义 `create_object` 的触发条件、执行流程与性能边界。
- [Native Class Object 设计](NATIVE_CLASS_OBJECT.md) 与 [实现验收矩阵](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。 - [Native Class Object 设计](NATIVE_CLASS_OBJECT.md) 与 [实现验收矩阵](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。
- [PHP 8.4 Property Hook 集成设计](PROPERTY_HOOKS.md):编译期 lowering、Zend Hook 元数据、对象内省及 PHPX ABI 边界。 - [PHP 8.4 Property Hook 集成设计](PROPERTY_HOOKS.md):编译期 lowering、Zend Hook 元数据、对象内省及 PHPX ABI 边界。
- [Interface Property Hook 实现方案](INTERFACE_PROPERTY_HOOKS.md):接口属性契约、编译期方差检查及 PHP 8.4 抽象 Hook 元数据。 - [Interface Property Hook 实现方案](INTERFACE_PROPERTY_HOOKS.md):接口属性契约、编译期方差检查及 PHP 8.4 抽象 Hook 元数据。

@ -1,7 +1,7 @@
<?php <?php
use native_types; use native_types;
function simple() function simple(): void
{ {
$a = 0; $a = 0;
$total_count = 10000000; $total_count = 10000000;
@ -19,7 +19,7 @@ function simple()
/****/ /****/
function simplecall() function simplecall(): void
{ {
$total = 0; $total = 0;
for ($i = 0; $i < 1000000; $i++) for ($i = 0; $i < 1000000; $i++)
@ -29,28 +29,28 @@ function simplecall()
/****/ /****/
function hallo($a) { function hallo(string $a): void {
} }
function simpleucall() { function simpleucall(): void {
for ($i = 0; $i < 1000000; $i++) for ($i = 0; $i < 1000000; $i++)
hallo("hallo"); hallo("hallo");
} }
/****/ /****/
function hallo2($a) { function hallo2(string $a): void {
} }
function simpleudcall() { function simpleudcall(): void {
for ($i = 0; $i < 1000000; $i++) for ($i = 0; $i < 1000000; $i++)
hallo2("hallo"); hallo2("hallo");
} }
/****/ /****/
function mandel() { function mandel(): void {
$w1=50; $w1=50;
$h1=150; $h1=150;
$recen=-.45; $recen=-.45;
@ -90,7 +90,7 @@ function mandel() {
/****/ /****/
function mandel2() { function mandel2(): void {
$b = " .:,;!/>)|&IH%*#"; $b = " .:,;!/>)|&IH%*#";
//float r, i, z, Z, t, c, C; //float r, i, z, Z, t, c, C;
for ($y=30; printf("\n"), $C = $y*0.1 - 1.5, $y--;){ for ($y=30; printf("\n"), $C = $y*0.1 - 1.5, $y--;){
@ -104,20 +104,20 @@ function mandel2() {
/****/ /****/
function Ack(int $m, int $n){ function Ack(int $m, int $n): int {
if($m == 0) return $n+1; if($m == 0) return $n+1;
if($n == 0) return Ack($m-1, 1); if($n == 0) return Ack($m-1, 1);
return Ack($m - 1, Ack($m, ($n - 1))); return Ack($m - 1, Ack($m, ($n - 1)));
} }
function ackermann(int $n) { function ackermann(int $n): void {
$r = Ack(3, $n); $r = Ack(3, $n);
print "Ack(3,$n): $r\n"; print "Ack(3,$n): $r\n";
} }
/****/ /****/
function ary($n) { function ary(int $n): void {
for ($i=0; $i<$n; $i++) { for ($i=0; $i<$n; $i++) {
$X[$i] = $i; $X[$i] = $i;
} }
@ -130,7 +130,7 @@ function ary($n) {
/****/ /****/
function ary2($n) { function ary2(int $n): void {
for ($i=0; $i<$n;) { for ($i=0; $i<$n;) {
$X[$i] = $i; ++$i; $X[$i] = $i; ++$i;
$X[$i] = $i; ++$i; $X[$i] = $i; ++$i;
@ -163,7 +163,7 @@ function ary2($n) {
/****/ /****/
function ary3(int $n) { function ary3(int $n): void {
for ($i=0; $i<$n; $i++) { for ($i=0; $i<$n; $i++) {
$X[$i] = $i + 1; $X[$i] = $i + 1;
$Y[$i] = 0; $Y[$i] = 0;
@ -190,7 +190,7 @@ function fibo(int $n): void {
/****/ /****/
function hash1(int $n) { function hash1(int $n): void {
for ($i = 1; $i <= $n; $i++) { for ($i = 1; $i <= $n; $i++) {
$X[dechex($i)] = $i; $X[dechex($i)] = $i;
} }
@ -203,7 +203,7 @@ function hash1(int $n) {
/****/ /****/
function hash2(int $n) { function hash2(int $n): void {
for ($i = 0; $i < $n; $i++) { for ($i = 0; $i < $n; $i++) {
$hash1["foo_$i"] = $i; $hash1["foo_$i"] = $i;
$hash2["foo_$i"] = 0; $hash2["foo_$i"] = 0;
@ -218,12 +218,12 @@ function hash2(int $n) {
/****/ /****/
function gen_random (int $n) { function gen_random(int $n): float {
global $LAST; global $LAST;
return( ($n * ($LAST = ($LAST * IA + IC) % IM)) / IM ); return( ($n * ($LAST = ($LAST * IA + IC) % IM)) / IM );
} }
function heapsort_r(int $n, &$ra) { function heapsort_r(int $n, array &$ra): void {
$l = ($n >> 1) + 1; $l = ($n >> 1) + 1;
$ir = $n; $ir = $n;
@ -255,7 +255,7 @@ function heapsort_r(int $n, &$ra) {
} }
} }
function heapsort(int $N) { function heapsort(int $N): void {
global $LAST; global $LAST;
define("IM", 139968); define("IM", 139968);
@ -272,7 +272,7 @@ function heapsort(int $N) {
/****/ /****/
function mkmatrix ($rows, $cols) { function mkmatrix(int $rows, int $cols): array {
$count = 1; $count = 1;
$mx = array(); $mx = array();
for ($i=0; $i<$rows; $i++) { for ($i=0; $i<$rows; $i++) {
@ -283,7 +283,7 @@ function mkmatrix ($rows, $cols) {
return ($mx); return ($mx);
} }
function mmult ($rows, $cols, $m1, $m2) { function mmult(int $rows, int $cols, array $m1, array $m2): array {
$m3 = array(); $m3 = array();
for ($i=0; $i<$rows; $i++) { for ($i=0; $i<$rows; $i++) {
for ($j=0; $j<$cols; $j++) { for ($j=0; $j<$cols; $j++) {
@ -297,7 +297,7 @@ function mmult ($rows, $cols, $m1, $m2) {
return($m3); return($m3);
} }
function matrix(int $n) { function matrix(int $n): void {
$SIZE = 30; $SIZE = 30;
$m1 = mkmatrix($SIZE, $SIZE); $m1 = mkmatrix($SIZE, $SIZE);
$m2 = mkmatrix($SIZE, $SIZE); $m2 = mkmatrix($SIZE, $SIZE);
@ -309,7 +309,7 @@ function matrix(int $n) {
/****/ /****/
function nestedloop($n) { function nestedloop(int $n): void {
$x = 0; $x = 0;
for ($a=0; $a<$n; $a++) for ($a=0; $a<$n; $a++)
for ($b=0; $b<$n; $b++) for ($b=0; $b<$n; $b++)
@ -323,7 +323,7 @@ function nestedloop($n) {
/****/ /****/
function sieve(int $n) { function sieve(int $n): void {
$count = 0; $count = 0;
while ($n-- > 0) { while ($n-- > 0) {
$count = 0; $count = 0;
@ -342,7 +342,7 @@ function sieve(int $n) {
/****/ /****/
function strcat($n) { function strcat(int $n): void {
$str = ""; $str = "";
while ($n-- > 0) { while ($n-- > 0) {
$str .= "hello\n"; $str .= "hello\n";
@ -359,13 +359,13 @@ function gethrtime(): float
return (($hrtime[0] * 1000000000.0 + $hrtime[1]) / 1000000000.0); return (($hrtime[0] * 1000000000.0 + $hrtime[1]) / 1000000000.0);
} }
function start_test() function start_test(): float
{ {
ob_start(); ob_start();
return gethrtime(); return gethrtime();
} }
function end_test($start, $name) function end_test(float $start, string $name): float
{ {
global $total; global $total;
$end = gethrtime(); $end = gethrtime();
@ -379,7 +379,7 @@ function end_test($start, $name)
return gethrtime(); return gethrtime();
} }
function total() function total(): void
{ {
global $total; global $total;
$pad = str_repeat("-", 24); $pad = str_repeat("-", 24);
@ -389,7 +389,7 @@ function total()
echo "Total" . $pad . $num . "\n"; echo "Total" . $pad . $num . "\n";
} }
function main() function main(): void
{ {
if (function_exists("date_default_timezone_set")) { if (function_exists("date_default_timezone_set")) {
date_default_timezone_set("UTC"); date_default_timezone_set("UTC");

@ -348,7 +348,7 @@ function main()
$x->call(N); $x->call(N);
$t = end_test($t, '$this->f()', $overhead); $t = end_test($t, '$this->f()', $overhead);
$x->read_const(N); $x->read_const(N);
$t = end_test($t, '$x = Foo::TEST', $overhead); $t = end_test($t, '$x = $this::TEST', $overhead);
create_object(N); create_object(N);
$t = end_test($t, 'new Foo()', $overhead); $t = end_test($t, 'new Foo()', $overhead);
read_const(N); read_const(N);

@ -0,0 +1,16 @@
<?php
class ClassConstantCodegen
{
public const VALUE = 23;
public function readThis(): int
{
return $this::VALUE;
}
}
function readDynamicClassConstant(object $target): mixed
{
return $target::VALUE;
}

@ -0,0 +1,34 @@
<?php
use native_types;
function hotPathTrace(string $value): string
{
return $value;
}
function hotPathCodegen(int $limit): int
{
$items = [];
$other = [2];
$value = 1;
$items[0] = $value;
$items[] = $value;
$items[0] += $value;
$items[0] += $other[0];
$items[2] = $other[0];
$assigned = ($items[1] = $value);
$assigned += ($items[0] += $value);
$safe = 'item-' . $limit;
$ordered = hotPathTrace('left') . hotPathTrace('right');
$length = strlen('item-' . $limit);
while ($limit-- > 0) {
++$value;
}
return $assigned + $length + strlen($safe) + strlen($ordered) + $value;
}

@ -0,0 +1,70 @@
<?php
class KnownNewObjectCodegen
{
public int $value = 0;
}
class EmptyArrayDefaultCodegen
{
public array $values = [];
}
class RuntimeArrayDefaultCodegen
{
public int $scalar = 7;
public array $values = [1];
public array $labels = ['default'];
}
class ScalarExpressionDefaultCodegen
{
public int $integer = 20 + 3;
public string $string = 'type' . 'php';
}
class ScalarConstantDefaultCodegen
{
private const VALUE = 23;
public int $integer = self::VALUE;
}
enum EnumPropertyDefaultCaseCodegen
{
case First;
}
class EnumPropertyDefaultCodegen
{
public EnumPropertyDefaultCaseCodegen $value = EnumPropertyDefaultCaseCodegen::First;
}
class HookOnlyDefaultCodegen
{
public string $value {
get => $this->value;
set => $value;
}
}
class AsymmetricOnlyDefaultCodegen
{
public private(set) int $value = 0;
}
function createKnownObjects(int $count): void
{
for ($i = 0; $i < $count; ++$i) {
$object = new KnownNewObjectCodegen();
}
}
function createRuntimeObject(): object
{
return new RuntimeProvidedObjectCodegen();
}
function main(): void
{
}

@ -0,0 +1,34 @@
<?php
use TypePhp\CompilerTest;
final class ClassConstantCodegenTest extends \BaseTest
{
public function testThisConstantIsExpandedButUnknownObjectUsesRuntimeLookup(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression(
'/php_classconstantcodegen__readthis\([^)]*\)[\s\S]*?= \(23L\);/',
$code,
);
self::assertStringContainsString('php::classConstant(', $code);
self::assertStringNotContainsString('php::constant(php::concat({', $code);
}
private function compileFixture(): string
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/class-constant-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
return $code;
}
}

@ -0,0 +1,54 @@
<?php
use TypePhp\CompilerTest;
final class HotPathCodegenTest extends \BaseTest
{
public function testKnownArrayStatementWritesAvoidResultTemporaries(): void
{
$code = $this->compileFixture();
self::assertStringContainsString('items.item(0L, true) = value;', $code);
self::assertStringContainsString('items.append(value);', $code);
self::assertStringContainsString('items.item(0L, true) += value;', $code);
self::assertStringContainsString('items.item(0L, true) += other.item(0L, false);', $code);
self::assertStringContainsString('items.item(2L, true) = other.item(0L, false);', $code);
self::assertStringContainsString('items.offsetSet(0L,', $code);
}
public function testSafeTwoOperandConcatAndExactStringArgumentStayUnboxed(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression(
'/php::concat\(_literal_strings\[\d+\], limit\)/',
$code,
);
self::assertStringContainsString('php::concat({', $code);
self::assertStringNotContainsString('php::fn::strlen(php::toString(php::concat(', $code);
}
public function testNativePostDecrementConditionUsesNativeTemporary(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression('/php::Int (tmp_var_\d+) = 0;[\s\S]*?\\1 = php::toInt\(limit--\);/', $code);
self::assertDoesNotMatchRegularExpression('/php::Var (tmp_var_\d+);[\s\S]*?\\1 = limit--;/', $code);
}
private function compileFixture(): string
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/hot-path-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
return $code;
}
}

@ -117,6 +117,6 @@ final class LocalVariableInitializerTest extends \BaseTest
self::assertStringContainsString('php::Var runtimeClassConstant;', $code); self::assertStringContainsString('php::Var runtimeClassConstant;', $code);
self::assertStringContainsString('runtimeClassConstant = php::constant(', $code); self::assertStringContainsString('runtimeClassConstant = php::constant(', $code);
self::assertStringContainsString('php::Var dynamicClass;', $code); self::assertStringContainsString('php::Var dynamicClass;', $code);
self::assertStringContainsString('dynamicClass = php::constant(', $code); self::assertStringContainsString('dynamicClass = php::classConstant(', $code);
} }
} }

@ -86,7 +86,9 @@ class NativePropertyTest extends \BaseTest
$code = file_get_contents($outputFile); $code = file_get_contents($outputFile);
$this->assertStringContainsString('typephp_static_int_ref(this_.attr(', $code); $this->assertStringContainsString('typephp_static_int_ref(this_.attr(', $code);
$this->assertStringContainsString('&= (~php::toInt(php::constant(', $code); // A TypePHP class constant is available during conversion and is
// folded before the native property operation is emitted.
$this->assertStringContainsString('&= (~php::toInt(1L));', $code);
} }
public function testNativePropertyWriteConvertsOnlyWhenTypesDiffer(): void public function testNativePropertyWriteConvertsOnlyWhenTypesDiffer(): void

@ -0,0 +1,134 @@
<?php
use TypePhp\CompilerTest;
final class NewObjectCodegenTest extends \BaseTest
{
public function testStableClassEntryLookupIsHoistedOutOfObjectCreationLoop(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression(
'/php_createknownobjects\([^)]*\) \{[\s\S]*?zend_class_entry \*(tmp_var_\d+) = php_get_persistent_class\([^;]+;[\s\S]*?php::newObject\(\1\)/',
$code,
);
self::assertStringNotContainsString(
'php::newObject(php_get_persistent_class(',
$code,
);
}
public function testRuntimeProvidedClassStillResolvesAtNewExpression(): void
{
$code = $this->compileFixture();
self::assertMatchesRegularExpression(
'/php_createruntimeobject\([^)]*\)[\s\S]*?php::newObject\(php_get_class\(/',
$code,
);
}
public function testStubRepresentablePropertyDefaultsUseZendDefaultTable(): void
{
[, $extension] = $this->compileFixtureAndExtension();
self::assertStringNotContainsString(
'create_object_KnownNewObjectCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'create_object_EmptyArrayDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'create_object_ScalarExpressionDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'create_object_ScalarConstantDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringContainsString(
'create_object_RuntimeArrayDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'zend_update_property(php_class_entry_RuntimeArrayDefaultCodegen, obj, ZEND_STRL("scalar")',
$extension,
);
self::assertStringContainsString(
'create_object_EnumPropertyDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'create_object_HookOnlyDefaultCodegen = php_get_create_object_fn',
$extension,
);
self::assertStringNotContainsString(
'create_object_AsymmetricOnlyDefaultCodegen = php_get_create_object_fn',
$extension,
);
}
public function testRuntimeArrayDefaultsUseLazyRequestTemplates(): void
{
[, $extension] = $this->compileFixtureAndExtension();
self::assertStringContainsString(
'THREAD_LOCAL bool php_request_array_defaults_initialized_RuntimeArrayDefaultCodegen = false;',
$extension,
);
self::assertStringContainsString(
'THREAD_LOCAL php::Var php_request_array_default_runtimearraydefaultcodegen__values;',
$extension,
);
self::assertStringContainsString(
'THREAD_LOCAL php::Var php_request_array_default_runtimearraydefaultcodegen__labels;',
$extension,
);
self::assertMatchesRegularExpression(
'/if \(UNEXPECTED\(!php_request_array_defaults_initialized_RuntimeArrayDefaultCodegen\)\) \{[\s\S]*prepared_default_0[\s\S]*prepared_default_1[\s\S]*php_request_array_defaults_initialized_RuntimeArrayDefaultCodegen = true;/',
$extension,
);
self::assertMatchesRegularExpression(
'/create_object_RuntimeArrayDefaultCodegen[^=]*= \[\][\s\S]*php_ensure_request_array_defaults_RuntimeArrayDefaultCodegen\(\);[\s\S]*typephp_create_object_with_defaults/',
$extension,
);
self::assertStringContainsString(
'= php_request_array_default_runtimearraydefaultcodegen__values;',
$extension,
);
self::assertStringContainsString(
'php_request_array_default_runtimearraydefaultcodegen__values.unset();',
$extension,
);
self::assertStringContainsString(
'php_request_array_default_runtimearraydefaultcodegen__labels.unset();',
$extension,
);
}
private function compileFixture(): string
{
return $this->compileFixtureAndExtension()[0];
}
/** @return array{string, string} */
private function compileFixtureAndExtension(): array
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/new-object-codegen.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
$extension = file_get_contents($compiler->genExtension());
self::assertIsString($code);
self::assertIsString($extension);
return [$code, $extension];
}
}

@ -50,10 +50,10 @@ class OperatorTest extends \BaseTest
$cppFile = $compiler->convertFile($testFile); $cppFile = $compiler->convertFile($testFile);
$cpp = file_get_contents($cppFile); $cpp = file_get_contents($cppFile);
$this->assertMatchesRegularExpression( // Ordered operands may be materialized before the return statement,
'/return php::toBool\(php::call\([^\n]+\)\);/', // but the dynamic call result must still cross an explicit Bool
$cpp, // conversion boundary before C++ logical operators consume it.
); $this->assertStringContainsString('php::toBool(php::call(', $cpp);
} }
public function testLiteralIntDivideByZeroDoesNotCompile(): void public function testLiteralIntDivideByZeroDoesNotCompile(): void

@ -18,7 +18,7 @@ final class StringConcatAssignTest extends \BaseTest
self::assertIsString($code); self::assertIsString($code);
self::assertGreaterThanOrEqual(3, substr_count($code, 'value.append(')); self::assertGreaterThanOrEqual(3, substr_count($code, 'value.append('));
self::assertStringContainsString('value.append(php::concat({', $code); self::assertStringContainsString('value.append(php::concat(', $code);
self::assertStringNotContainsString('value = php::concat({value,', $code); self::assertStringNotContainsString('value = php::concat({value,', $code);
} }
} }

@ -1337,6 +1337,25 @@ class CompilerBase implements PropertyAccessContext
return $helper . '(' . $id . ', ' . $this->getLiteralString($className) . ')'; return $helper . '(' . $id . ', ' . $this->getLiteralString($className) . ')';
} }
/**
* Resolve a process-stable class once at function entry. This keeps class
* table/cache lookup out of loops containing `new KnownClass()` while
* leaving runtime-provided classes at their original expression site.
*/
protected function getLocalClassEntryPtr(string $className): string
{
if (!$this->isProcessStableClass($className)) {
return $this->getClassEntryPtr($className);
}
if (isset($this->context->classEntryPtrs[$className])) {
return $this->context->classEntryPtrs[$className];
}
$entry = $this->genTmpVarName();
$this->context->classEntryPtrs[$className] = $entry;
return $entry;
}
/** The declaring class controls visibility; the runtime called class does not. */ /** The declaring class controls visibility; the runtime called class does not. */
protected function getCallableScopeExpr(): string protected function getCallableScopeExpr(): string
{ {
@ -3859,7 +3878,7 @@ class CompilerBase implements PropertyAccessContext
. $this->getNativeObjectInitializerName($className) . '(this_); ' . $this->getNativeObjectInitializerName($className) . '(this_); '
. self::PREFIX . $nativeCtor . '(this_' . $nativeArgs . '); })'; . self::PREFIX . $nativeCtor . '(this_' . $nativeArgs . '); })';
} }
$cePtr = $this->getClassEntryPtr($className); $cePtr = $this->getLocalClassEntryPtr($className);
} }
} else { } else {
$cePtr = $className; $cePtr = $className;
@ -5102,6 +5121,10 @@ class CompilerBase implements PropertyAccessContext
. ', this_);' . PHP_EOL; . ', this_);' . PHP_EOL;
} }
$code .= $this->genLocalVarDecl($this->context->localVars); $code .= $this->genLocalVarDecl($this->context->localVars);
foreach ($this->context->classEntryPtrs as $className => $entry) {
$code .= $this->getIndent() . 'zend_class_entry *' . $entry . ' = '
. $this->getClassEntryPtr($className) . ';' . PHP_EOL;
}
if ($this->context->nativeObjects !== []) { if ($this->context->nativeObjects !== []) {
$rootSlots = []; $rootSlots = [];
foreach ($this->context->nativeObjects as $name => $_class) { foreach ($this->context->nativeObjects as $name => $_class) {

@ -68,6 +68,8 @@ class FunctionContext
* @var array<string, string> * @var array<string, string>
*/ */
public array $ceWrappers = []; public array $ceWrappers = [];
/** @var array<string, string> Process-stable class name => function-local zend_class_entry* variable. */
public array $classEntryPtrs = [];
/** Reusable php::CallableScope local, created only when this function performs scoped calls. */ /** Reusable php::CallableScope local, created only when this function performs scoped calls. */
public ?string $callableScopeVar = null; public ?string $callableScopeVar = null;
/** This generated body needs a temporary lexical scope on the nearest user-code frame. */ /** This generated body needs a temporary lexical scope on the nearest user-code frame. */
@ -124,6 +126,7 @@ class FunctionContext
$this->unsafeObjectProps = []; $this->unsafeObjectProps = [];
$this->staticPropRefs = []; $this->staticPropRefs = [];
$this->ceWrappers = []; $this->ceWrappers = [];
$this->classEntryPtrs = [];
$this->callableScopeVar = null; $this->callableScopeVar = null;
$this->tmpVarIndex = 0; $this->tmpVarIndex = 0;
$this->scopeLayouts = []; $this->scopeLayouts = [];
@ -167,6 +170,7 @@ class FunctionContext
$this->objectProps = []; $this->objectProps = [];
$this->hoistedProps = []; $this->hoistedProps = [];
$this->staticPropRefs = []; $this->staticPropRefs = [];
$this->classEntryPtrs = [];
$this->scopeLayouts = []; $this->scopeLayouts = [];
$this->scopeLevel = 0; $this->scopeLevel = 0;
$this->inLoop = false; $this->inLoop = false;

@ -27,6 +27,10 @@ class PropertyDef
public string $typeStr = ''; public string $typeStr = '';
public bool $promoted = false; public bool $promoted = false;
public bool $readonly = false; public bool $readonly = false;
/** The generated Zend property table cannot represent this default exactly. */
public bool $requiresRuntimeDefaultInit = false;
/** Cached declared-property offset used by the runtime default initializer. */
public string $runtimeDefaultOffset = '';
public ?string $getter = null; public ?string $getter = null;
public ?string $setter = null; public ?string $setter = null;
public bool $virtual = false; public bool $virtual = false;

@ -402,15 +402,51 @@ trait FuncCallOptimizer
protected function resolveArg(Node\Expr\FuncCall $expr, int $index, string $type): string protected function resolveArg(Node\Expr\FuncCall $expr, int $index, string $type): string
{ {
$base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type; $base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type;
$arg = $expr->args[$index]->value;
$raw = ($base === self::ARG_TYPE_REF) ? $this->getRefArg($expr, $index) : $this->getArg($expr, $index);
if ($base === self::ARG_TYPE_ARRAY) { if ($base === self::ARG_TYPE_ARRAY) {
return $this->convertStdContainerArrayExpr($expr, $index, $this->getArg($expr, $index)); if ($this->argumentAlreadyHasExactType($arg, Type::ARRAY)) {
return $raw;
}
return $this->convertStdContainerArrayExpr($expr, $index, $raw);
}
$exactType = match ($base) {
self::ARG_TYPE_STR => Type::STR,
self::ARG_TYPE_INT => Type::INT,
self::ARG_TYPE_FLOAT => Type::FLOAT,
self::ARG_TYPE_BOOL => Type::BOOL,
default => null,
};
if ($exactType !== null && $this->argumentAlreadyHasExactType($arg, $exactType)) {
return $raw;
} }
$raw = ($base === self::ARG_TYPE_REF) ? $this->getRefArg($expr, $index) : $this->getArg($expr, $index);
return $this->applyArgConversion($raw, $type); return $this->applyArgConversion($raw, $type);
} }
protected function argumentAlreadyHasExactType(Node\Expr $arg, string $type): bool
{
if ($this->isVarExpr($arg)) {
return !$this->isStdContainer($arg->name)
&& $this->hasVar($arg->name)
&& $this->getVarType($arg->name) === $type;
}
// Semantic type information is not enough here: a dynamically read
// typed property, for example, is still represented by php::Variant.
// Limit the shortcut to expressions whose generated C++ value has a
// fixed representation without an implicit PHPX conversion.
$fixedRepresentation = $arg instanceof Node\Scalar
|| $arg instanceof Node\Expr\Cast
|| $arg instanceof Node\Expr\Array_
|| $arg instanceof Node\Expr\BinaryOp\Concat
|| $arg instanceof Node\Expr\ConstFetch;
return $fixedRepresentation && $this->detectTypeOfExpr($arg) === $type;
}
protected function applyArgConversion(string $cxxExpr, string $type): string protected function applyArgConversion(string $cxxExpr, string $type): string
{ {
$base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type; $base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type;

@ -19,7 +19,11 @@ use PhpParser\NodeAbstract;
trait AssignOpTrait trait AssignOpTrait
{ {
protected function parseAssignArrayDim(NodeAbstract $left, NodeAbstract $right): string protected function parseAssignArrayDim(
NodeAbstract $left,
NodeAbstract $right,
bool $resultUnused = false,
): string
{ {
if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) { if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) {
$this->assertNotNativeObjectArrayKey($left->dim); $this->assertNotNativeObjectArrayKey($left->dim);
@ -58,9 +62,6 @@ trait AssignOpTrait
$value = $this->parseExprAsValue($right); $value = $this->parseExprAsValue($right);
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, Type::VAR);
// item(dim, true) updates an existing reference's value, while offsetSet() // item(dim, true) updates an existing reference's value, while offsetSet()
// replaces the array bucket and breaks the reference. Keep offsetSet() for // replaces the array bucket and breaks the reference. Keep offsetSet() for
// ArrayAccess objects; dynamically typed/reference containers need a // ArrayAccess objects; dynamically typed/reference containers need a
@ -68,10 +69,26 @@ trait AssignOpTrait
$arrayType = $this->getVarType($array); $arrayType = $this->getVarType($array);
if ($left->dim === null) { if ($left->dim === null) {
if ($resultUnused
&& $arrayType === Type::ARRAY
&& $this->canEmitDirectArrayWriteOperand($right)
) {
return $code . $array . '.append(' . $value . ')';
}
$tmp = $this->addTmpVar(Type::VAR);
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')'; return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')';
} }
$dim = $this->parseIdentifier($left->dim); $dim = $this->parseIdentifier($left->dim);
if ($resultUnused
&& $arrayType === Type::ARRAY
&& !$this->shouldMaterializeOrderedOperand($left->dim)
&& $this->canEmitDirectArrayWriteOperand($right)
) {
return $code . $array . '.item(' . $dim . ', true) = ' . $value;
}
$tmp = $this->addTmpVar(Type::VAR);
if ($arrayType === Type::ARRAY) { if ($arrayType === Type::ARRAY) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')'; return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')';
} }
@ -156,6 +173,7 @@ trait AssignOpTrait
$left, $left,
$right, $right,
$this->canFoldLocalInitializerIntoDeclaration($v), $this->canFoldLocalInitializerIntoDeclaration($v),
$v->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false),
); );
} }
@ -283,6 +301,7 @@ trait AssignOpTrait
Expr $left, Expr $left,
Expr $right, Expr $right,
bool $foldIntoDeclaration = false, bool $foldIntoDeclaration = false,
bool $resultUnused = false,
): string ): string
{ {
$this->assertImmutableMutationTarget($left); $this->assertImmutableMutationTarget($left);
@ -635,7 +654,7 @@ trait AssignOpTrait
if ($this->isStdContainerExpr($left)) { if ($this->isStdContainerExpr($left)) {
return $this->parseStdContainerAssign($left, $right); return $this->parseStdContainerAssign($left, $right);
} }
return $this->parseAssignArrayDim($left, $right); return $this->parseAssignArrayDim($left, $right, $resultUnused);
} elseif ($this->isArrayDimFetch($left) and $this->isPropertyFetch($left->var)) { } elseif ($this->isArrayDimFetch($left) and $this->isPropertyFetch($left->var)) {
return $this->parseAssignPropertyArrayDim($left, $right); return $this->parseAssignPropertyArrayDim($left, $right);
} elseif ($this->isArrayDimFetch($left) and $this->isStaticPropertyFetch($left->var)) { } elseif ($this->isArrayDimFetch($left) and $this->isStaticPropertyFetch($left->var)) {
@ -891,6 +910,12 @@ trait AssignOpTrait
if ($this->isStdContainerExpr($node->var)) { if ($this->isStdContainerExpr($node->var)) {
return $this->parseStdContainerAssignOp($node, $op); return $this->parseStdContainerAssignOp($node, $op);
} }
if ($this->canUpdateKnownArraySlotInPlace($node, $op)) {
$array = $this->parseWritableIdentifier($node->var->var);
$dim = $this->parseIdentifier($node->var->dim);
return $array . '.item(' . $dim . ', true) ' . $op . ' '
. $this->parseExprAsValue($node->expr);
}
/** /**
* $count[$r] -= 1; * $count[$r] -= 1;
* 需要转为下面语句: * 需要转为下面语句:
@ -1095,6 +1120,41 @@ trait AssignOpTrait
return 'php::' . $class . '::' . $method . '(' . $leftExpr . ', ' . $convertedRight . ')'; return 'php::' . $class . '::' . $method . '(' . $leftExpr . ', ' . $convertedRight . ')';
} }
private function canUpdateKnownArraySlotInPlace(Expr\AssignOp $node, string $op): bool
{
if (!$node->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false)
|| !$node->var instanceof Expr\ArrayDimFetch
|| $node->var->dim === null
|| !$this->isVarExpr($node->var->var)
|| $this->shouldMaterializeOrderedOperand($node->var->dim)
|| !$this->canEmitDirectArrayWriteOperand($node->expr)
) {
return false;
}
$array = $this->parseIdentifier($node->var->var);
return $this->hasVar($array)
&& $this->getVarType($array) === Type::ARRAY
&& in_array($op, ['+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '&=', '|=', '^='], true);
}
private function canEmitDirectArrayWriteOperand(NodeAbstract $expr): bool
{
if (!$this->shouldMaterializeOrderedOperand($expr)) {
return true;
}
if (!$expr instanceof Expr\ArrayDimFetch
|| $expr->dim === null
|| !$this->isVarExpr($expr->var)
|| $this->shouldMaterializeOrderedOperand($expr->dim)
) {
return false;
}
$array = $this->parseIdentifier($expr->var);
return $this->hasVar($array) && $this->getVarType($array) === Type::ARRAY;
}
protected function parseAssignOpConcat(Expr\AssignOp\Concat $expr): string protected function parseAssignOpConcat(Expr\AssignOp\Concat $expr): string
{ {
return $this->parseAssignOp($expr, '.='); return $this->parseAssignOp($expr, '.=');

@ -566,6 +566,20 @@ trait BinaryOpTrait
protected function getOrderedOperandTmpType(NodeAbstract $expr, string $value): string protected function getOrderedOperandTmpType(NodeAbstract $expr, string $value): string
{ {
if (
($expr instanceof Expr\PostInc
|| $expr instanceof Expr\PostDec
|| $expr instanceof Expr\PreInc
|| $expr instanceof Expr\PreDec)
&& $this->isVarExpr($expr->var)
) {
$type = $this->getVarType($this->parseIdentifier($expr->var));
if ($this->nativeTypes && $this->isNativeType($type)) {
return $type;
}
return Type::VAR;
}
if ( if (
$expr instanceof Expr\BinaryOp $expr instanceof Expr\BinaryOp
|| $expr instanceof Expr\FuncCall || $expr instanceof Expr\FuncCall
@ -647,6 +661,14 @@ trait BinaryOpTrait
$items = []; $items = [];
$this->flattenConcatExpr($expr, $items); $this->flattenConcatExpr($expr, $items);
// C++ does not order regular function arguments. The two-argument
// overload is therefore only safe for scalar operands whose parsing
// and conversion cannot execute user code or mutate state. All other
// concatenations use the braced-list overload, whose elements are
// sequenced left-to-right by C++17.
$useTwoOperandOverload = $prefixExpressions === []
&& $this->canUseTwoOperandConcatOverload($items);
$argList = $prefixExpressions; $argList = $prefixExpressions;
foreach ($items as $item) { foreach ($items as $item) {
// Keep one operand so concat still performs PHP string coercion. // Keep one operand so concat still performs PHP string coercion.
@ -672,9 +694,37 @@ trait BinaryOpTrait
$argList[] = $this->prepareConcatOperand($parsed, $type); $argList[] = $this->prepareConcatOperand($parsed, $type);
} }
if ($useTwoOperandOverload && count($argList) === 2) {
return Symbol::concat() . '(' . $argList[0] . ', ' . $argList[1] . ')';
}
return Symbol::concat() . '({' . implode(', ', $argList) . '})'; return Symbol::concat() . '({' . implode(', ', $argList) . '})';
} }
protected function canUseTwoOperandConcatOverload(array $items): bool
{
if (count($items) !== 2) {
return false;
}
foreach ($items as $item) {
// Even a scalar-typed binary expression may emit a warning or
// throw (division by zero, failed conversion, overloaded object
// operation). Restrict the unordered overload to values that are
// already materialized and cannot execute during argument setup.
if (!($item instanceof Node\Scalar || $this->isVarExpr($item))
|| $this->shouldMaterializeOrderedOperand($item)
|| !in_array(
$this->detectTypeOfExpr($item),
[Type::STR, Type::INT, Type::FLOAT, Type::BOOL],
true,
)
) {
return false;
}
}
return true;
}
protected function prepareConcatOperand(string $expr, string $type): string protected function prepareConcatOperand(string $expr, string $type): string
{ {
if (in_array($type, [Type::STR, Type::INT, Type::FLOAT, Type::BOOL], true)) { if (in_array($type, [Type::STR, Type::INT, Type::FLOAT, Type::BOOL], true)) {

@ -177,14 +177,41 @@ trait ClassConstantFetchTrait
protected function parseDynamicClassConstFetch(Expr\ClassConstFetch $expr): string protected function parseDynamicClassConstFetch(Expr\ClassConstFetch $expr): string
{ {
$const = $this->escapeString($this->parseIdentifier($expr->name)); $const = $this->escapeString($this->parseIdentifier($expr->name));
// PhpParser represents `$this::CONST` as a dynamic class target even
// though the receiver class is the class currently being compiled.
// TypePHP class constants are compile-time data, so do not route this
// common path through get_class(), string concatenation and ZendVM's
// global constant table.
if ($this->isVarExpr($expr->class)
&& $this->parseVariable($expr->class) === 'this_'
&& $this->classDef
&& $this->hasClass($this->getFullClassName())
) {
$nativeConst = $this->findNativeClassConst(
$expr,
$this->getFullClassName(),
$const,
);
if ($nativeConst !== false) {
return $nativeConst;
}
}
$target = $this->materializeDynamicClassConstTarget($expr->class); $target = $this->materializeDynamicClassConstTarget($expr->class);
if ($const === 'class') { if ($const === 'class') {
return 'php::fn::get_class(' . $target . ')'; return 'php::fn::get_class(' . $target . ')';
} }
$className = '(' . $target . '.isObject() ? php::fn::get_class(' . $target . ') : ' . $target . ')'; $scope = $this->methodDef && $this->classDef
return Symbol::constant() . '(php::concat({' . $className . ', "::", ' . $this->getLiteralString($const) . '}))'; ? $this->getClassEntryPtr($this->getFullClassName())
: 'nullptr';
return 'php::classConstant('
. $target . ', '
. $this->getLiteralString($const) . ', '
. $scope
. ')';
} }
protected function parseDynamicClassConstNameFetch(Expr\ClassConstFetch $expr): string protected function parseDynamicClassConstNameFetch(Expr\ClassConstFetch $expr): string

@ -1435,6 +1435,7 @@ class Preprocessor extends CompilerBase
$propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY);
$propDef->class = $class; $propDef->class = $class;
$propDef->arrayInitPlan = $arrayInitPlan; $propDef->arrayInitPlan = $arrayInitPlan;
$propDef->requiresRuntimeDefaultInit = $this->propertyDefaultRequiresRuntimeInit($defaultNode);
$propDef->promoted = $promoted; $propDef->promoted = $promoted;
if ($typeNode instanceof NullableType || $typeNode instanceof UnionType || $typeNode instanceof IntersectionType) { if ($typeNode instanceof NullableType || $typeNode instanceof UnionType || $typeNode instanceof IntersectionType) {
$typeInfo = $this->buildTypeCheckFromNode($typeNode); $typeInfo = $this->buildTypeCheckFromNode($typeNode);
@ -1445,6 +1446,30 @@ class Preprocessor extends CompilerBase
return $propDef; return $propDef;
} }
/**
* gen_stub emits compile-time scalar values and empty arrays exactly into
* the internal class default-property table. Non-empty arrays are emitted
* there as an empty-array placeholder, while enum cases need a live object;
* both must therefore be restored by create_object. Keep an unresolved
* expression on that conservative runtime path as well.
*/
private function propertyDefaultRequiresRuntimeInit(?NodeAbstract $default): bool
{
if ($default === null) {
return false;
}
if ($default instanceof Node\Expr\Array_) {
return $default->items !== [];
}
// A null result includes enum cases and constants which cannot be
// resolved safely during preprocessing. Their runtime initialization
// must not be removed merely because their source syntax resembles a
// scalar constant expression.
$type = $this->detectDefaultValueType($default);
return $type === null || $type === 'array';
}
/** /**
* Diagnose, during preprocessing, whether a property's default value is * Diagnose, during preprocessing, whether a property's default value is
* compatible with its declared type. * compatible with its declared type.

@ -946,6 +946,7 @@ CODE;
} }
$code .= "// class \n"; $code .= "// class \n";
$code .= $this->genRequestArrayDefaultStorage();
foreach ($this->getClassLikesWithConstants() as $classDef) { foreach ($this->getClassLikesWithConstants() as $classDef) {
if ($classDef instanceof ClassDef if ($classDef instanceof ClassDef
&& !$classDef->nativeObject && !$classDef->nativeObject
@ -962,6 +963,7 @@ CODE;
} }
} }
} }
$code .= $this->genRequestArrayDefaultInitializers();
$code .= "// clang-format off\n"; $code .= "// clang-format off\n";
$code .= "static const zend_function_entry ext_functions[] = {\n"; $code .= "static const zend_function_entry ext_functions[] = {\n";
@ -1107,6 +1109,7 @@ CODE;
foreach ($this->nativeStaticInitializers as $name => $_) { foreach ($this->nativeStaticInitializers as $name => $_) {
$code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL; $code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL;
} }
$code .= $this->genRequestArrayDefaultCleanup();
foreach ($this->constants as $name => $const) { foreach ($this->constants as $name => $const) {
if ($const->type !== Type::VAR) { if ($const->type !== Type::VAR) {
continue; continue;
@ -1974,6 +1977,134 @@ CODE;
return implode(PHP_EOL, $lines) . PHP_EOL; return implode(PHP_EOL, $lines) . PHP_EOL;
} }
/** @return array<PropertyDef> */
private function getRequestArrayDefaultProperties(ClassDef $classDef): array
{
$properties = [];
foreach ($classDef->properties as $property) {
if (!$property->isStatic()
&& $property->requiresRuntimeDefaultInit
&& $property->arrayInitPlan !== null
) {
$properties[] = $property;
}
}
return $properties;
}
private function getRequestArrayDefaultInitializedName(ClassDef $classDef): string
{
return self::PREFIX . 'request_array_defaults_initialized_' . $classDef->getNamespacedName();
}
private function getRequestArrayDefaultInitializerName(ClassDef $classDef): string
{
return self::PREFIX . 'ensure_request_array_defaults_' . $classDef->getNamespacedName();
}
private function getRequestArrayDefaultTemplateName(ClassDef $classDef, PropertyDef $property): string
{
return self::PREFIX . 'request_array_default_'
. $this->getNativeName($property->name, $classDef->namespace, $classDef->name);
}
private function indentGeneratedBlock(string $code, int $level): string
{
$code = rtrim($code, "\r\n");
if ($code === '') {
return '';
}
$indent = str_repeat(' ', $level);
return $indent . str_replace("\n", "\n{$indent}", $code) . PHP_EOL;
}
private function genRequestArrayDefaultStorage(): string
{
$code = '';
foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait || $classDef->enum || $classDef->nativeObject) {
continue;
}
$properties = $this->getRequestArrayDefaultProperties($classDef);
if (!$properties) {
continue;
}
$code .= 'THREAD_LOCAL bool '
. $this->getRequestArrayDefaultInitializedName($classDef)
. ' = false;' . PHP_EOL;
foreach ($properties as $property) {
$code .= 'THREAD_LOCAL php::Var '
. $this->getRequestArrayDefaultTemplateName($classDef, $property)
. ';' . PHP_EOL;
}
}
return $code === '' ? '' : "// request array property defaults\n{$code}";
}
private function genRequestArrayDefaultInitializers(): string
{
$code = '';
foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait || $classDef->enum || $classDef->nativeObject) {
continue;
}
$properties = $this->getRequestArrayDefaultProperties($classDef);
if (!$properties) {
continue;
}
$initialized = $this->getRequestArrayDefaultInitializedName($classDef);
$code .= 'static inline void '
. $this->getRequestArrayDefaultInitializerName($classDef)
. '() {' . PHP_EOL;
$code .= " if (UNEXPECTED(!{$initialized})) {" . PHP_EOL;
foreach ($properties as $index => $_property) {
$code .= " php::Var prepared_default_{$index};" . PHP_EOL;
}
foreach ($properties as $index => $property) {
$plan = $property->arrayInitPlan;
$code .= ' do {' . PHP_EOL;
$code .= $this->indentGeneratedBlock($plan->init, 3);
$code .= " prepared_default_{$index} = {$plan->expr};" . PHP_EOL;
$code .= $this->indentGeneratedBlock($plan->clean, 3);
$code .= ' } while (0);' . PHP_EOL;
}
foreach ($properties as $index => $property) {
$template = $this->getRequestArrayDefaultTemplateName($classDef, $property);
$code .= " {$template} = std::move(prepared_default_{$index});" . PHP_EOL;
}
$code .= " {$initialized} = true;" . PHP_EOL;
$code .= ' }' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL;
}
return $code;
}
private function genRequestArrayDefaultCleanup(): string
{
$code = '';
foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait || $classDef->enum || $classDef->nativeObject) {
continue;
}
$properties = $this->getRequestArrayDefaultProperties($classDef);
if (!$properties) {
continue;
}
$initialized = $this->getRequestArrayDefaultInitializedName($classDef);
$code .= "if ({$initialized}) {" . PHP_EOL;
foreach ($properties as $property) {
$code .= ' '
. $this->getRequestArrayDefaultTemplateName($classDef, $property)
. '.unset();' . PHP_EOL;
}
$code .= " {$initialized} = false;" . PHP_EOL;
$code .= '}' . PHP_EOL;
}
return $code === '' ? '' : "// request array property defaults\n{$code}";
}
public function genClassPropertyInit(): string public function genClassPropertyInit(): string
{ {
$code = ''; $code = '';
@ -1988,41 +2119,64 @@ CODE;
if ($classDef && !$classDef->trait && !$classDef->enum) { if ($classDef && !$classDef->trait && !$classDef->enum) {
$className = $classDef->getNamespacedName(); $className = $classDef->getNamespacedName();
$handlers = "property_handlers_{$className}"; $handlers = "property_handlers_{$className}";
$requestArrayDefaults = $this->getRequestArrayDefaultProperties($classDef);
$ensureRequestArrayDefaults = $requestArrayDefaults
? $this->getRequestArrayDefaultInitializerName($classDef) . '();' . PHP_EOL
: '';
$initBlock = ''; $initBlock = '';
foreach ($classDef->properties as $property) { foreach ($classDef->properties as $property) {
if ($property->isStatic() || $property->default === null) { // Scalar/null/empty-array defaults already live in the
// Zend class default-property table generated by
// gen_stub.php. Only values represented there by a
// placeholder (for example non-empty arrays and enum
// cases) belong in the per-object initialization path.
if ($property->isStatic()
|| $property->default === null
|| !$property->requiresRuntimeDefaultInit
) {
continue; continue;
} }
if ($property->arrayInitPlan) { if ($property->arrayInitPlan) {
$init = "auto value = {$property->arrayInitPlan->expr};\n"; $initBlock .= 'target.attr(' . $property->runtimeDefaultOffset
$init .= 'zend_update_property(' . $ce . ', obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n"; . ', php::AttrMode::Update) = '
$init .= "php::throwErrorIfOccurred();\n"; . $this->getRequestArrayDefaultTemplateName($classDef, $property)
$initBlock .= $this->wrapArrayInitPlan($property->arrayInitPlan, $init); . ';' . PHP_EOL;
} else { } else {
// Scalar / constant / null default value. Wrap it in a // Runtime-only constant value (for example an enum
// php::Var so it can be stored as a zval in the object's // case). Wrap it in php::Var and write the already
// property table via zend_update_property. Each property is // resolved backing slot directly. Each property uses a
// wrapped in its own block so the local `value` does not // separate block so its local `value` cannot clash with
// clash with siblings declared in the same create_object body. // siblings in the same create_object body.
$default = $property->type === Type::FLOAT $default = $property->type === Type::FLOAT
? $this->convertFloatExpr($property->default) ? $this->convertFloatExpr($property->default)
: $property->default; : $property->default;
$init = "do {\n"; $init = "do {\n";
$init .= "auto value = php::Var({$default});\n"; $init .= "auto value = php::Var({$default});\n";
$init .= 'zend_update_property(' . $ce . ', obj, ' . $this->genZendStrl($property->name) . ", value.ptr());\n"; $init .= 'target.attr(' . $property->runtimeDefaultOffset
$init .= "php::throwErrorIfOccurred();\n"; . ", php::AttrMode::Update) = value;\n";
$init .= "} while (0);\n"; $init .= "} while (0);\n";
$initBlock .= $init; $initBlock .= $init;
} }
} }
$delegateToParentAllocator = $this->parentHasCustomCreateObjectOnPhp84($classDef); $delegateToParentAllocator = $this->parentHasCustomCreateObjectOnPhp84($classDef);
$buildCreateBody = function () use ($classDef, $className, $handlers, $initBlock, $delegateToParentAllocator): string { $buildCreateBody = function () use (
$classDef,
$className,
$handlers,
$ensureRequestArrayDefaults,
$initBlock,
$delegateToParentAllocator,
): string {
$body = $classDef->ctorInit; $body = $classDef->ctorInit;
$body .= $ensureRequestArrayDefaults;
$body .= "auto obj = typephp_create_object_with_defaults(\n"; $body .= "auto obj = typephp_create_object_with_defaults(\n";
$body .= "class_type, create_object_{$className}, "; $body .= "class_type, create_object_{$className}, ";
$body .= ($delegateToParentAllocator ? 'true' : 'false') . ",\n"; $body .= ($delegateToParentAllocator ? 'true' : 'false') . ",\n";
$body .= "[&](zend_object *obj) {\n"; $body .= "[&](zend_object *obj) {\n";
if ($initBlock !== '') {
$body .= "php::Object target{obj};\n";
}
$body .= $initBlock; $body .= $initBlock;
$body .= "});\n"; $body .= "});\n";
$body .= $classDef->ctorClean; $body .= $classDef->ctorClean;
@ -2030,7 +2184,7 @@ CODE;
}; };
$code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n"; $code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n";
if ($classDef->requireCtor || $this->classHasAsymmetricOrHookedProperty($classDef)) { if ($classDef->requireCtor) {
$code .= "create_object_{$className} = php_get_create_object_fn({$ce});\n"; $code .= "create_object_{$className} = php_get_create_object_fn({$ce});\n";
$code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n"; $code .= "{$ce}->create_object = [](zend_class_entry *class_type) -> zend_object* {\n";
$code .= $buildCreateBody(); $code .= $buildCreateBody();
@ -2041,46 +2195,6 @@ CODE;
return $code; return $code;
} }
/**
* Whether the given class (or any of its ancestors) declares an asymmetric
* visibility property (private(set)/protected(set)) or a hooked property
* (getter/setter). Such classes install a custom write_property handler, and
* on PHP >= 8.4 that handler lives in the class's default object handlers, so
* the engine's object_properties_init would reject inherited default values
* unless we generate our own create_object that initializes with the standard
* handlers first.
*/
private function classHasAsymmetricOrHookedProperty(ClassDef $classDef): bool
{
$current = $classDef;
$seen = [];
while ($current !== null) {
$key = strtolower(ltrim($current->getNamespacedName(), '\\'));
if (isset($seen[$key])) {
break;
}
$seen[$key] = true;
foreach ($current->properties as $property) {
if ($property->isPrivateSet()
|| $property->isProtectedSet()
|| $property->getter !== null
|| $property->setter !== null
) {
return true;
}
}
if (!$current->extends) {
break;
}
$parent = $this->getClassDef($current->extends);
if ($parent === null) {
break;
}
$current = $parent;
}
return false;
}
private function parentHasCustomCreateObjectOnPhp84(ClassDef $classDef): bool private function parentHasCustomCreateObjectOnPhp84(ClassDef $classDef): bool
{ {
if ($classDef->extends === '') { if ($classDef->extends === '') {
@ -2093,13 +2207,10 @@ CODE;
$parent = $this->getClassDef($classDef->extends); $parent = $this->getClassDef($classDef->extends);
while ($parent !== null) { while ($parent !== null) {
foreach ($parent->properties as $property) { foreach ($parent->properties as $property) {
if (!$property->isStatic() && $property->default !== null) { if (!$property->isStatic() && $property->requiresRuntimeDefaultInit) {
return true; return true;
} }
} }
if ($this->classHasAsymmetricOrHookedProperty($parent)) {
return true;
}
if ($parent->extends === '') { if ($parent->extends === '') {
break; break;
} }
@ -3655,7 +3766,11 @@ CODE;
} }
$defaultPropCount = 0; $defaultPropCount = 0;
foreach ($classDef->properties as $property) { foreach ($classDef->properties as $property) {
if (!$property->isStatic() && $property->default !== null) { if (!$property->isStatic() && $property->requiresRuntimeDefaultInit) {
$property->runtimeDefaultOffset = $this->getPropertyOffset(
$classDef->getNamespacedName(false),
$property->name,
);
$defaultPropCount++; $defaultPropCount++;
} }
} }

@ -0,0 +1,21 @@
--TEST--
known class entries can be reused across repeated object creation
--FILE--
<?php
class RepeatedObject
{
public int $value = 7;
}
function main(): void
{
$last = null;
for ($i = 0; $i < 3; ++$i) {
$last = new RepeatedObject();
}
var_dump($last->value);
}
?>
--EXPECT--
int(7)

@ -0,0 +1,35 @@
--TEST--
$this class constants resolve from the current TypePHP class
--FILE--
<?php
class ClassConstantParent
{
protected const INHERITED = 'parent';
}
class ClassConstantReader extends ClassConstantParent
{
public const VALUE = 23;
private const PRIVATE_VALUE = 'private';
public function values(): array
{
return [$this::VALUE, $this::PRIVATE_VALUE, $this::INHERITED];
}
}
function main(): void
{
var_dump((new ClassConstantReader())->values());
}
?>
--EXPECT--
array(3) {
[0]=>
int(23)
[1]=>
string(7) "private"
[2]=>
string(6) "parent"
}

@ -0,0 +1,59 @@
--TEST--
Property defaults use Zend table values and restore runtime-only defaults
--FILE--
<?php
enum DefaultInitializationState
{
case Ready;
}
class DefaultInitializationPaths
{
public int $scalar = 1 + 2;
public string $text = 'type' . 'php';
public array $empty = [];
public array $values = ['first'];
public DefaultInitializationState $state = DefaultInitializationState::Ready;
}
class DefaultInitializationException extends Exception
{
public array $context = ['runtime'];
}
function main(): void
{
$first = new DefaultInitializationPaths();
$second = new DefaultInitializationPaths();
$first->values[] = 'second';
var_dump($first->scalar, $first->text, $first->empty);
var_dump($first->values, $second->values);
var_dump($first->state === DefaultInitializationState::Ready);
$exception = new DefaultInitializationException('message');
var_dump($exception->getMessage(), $exception->context);
}
?>
--EXPECT--
int(3)
string(7) "typephp"
array(0) {
}
array(2) {
[0]=>
string(5) "first"
[1]=>
string(6) "second"
}
array(1) {
[0]=>
string(5) "first"
}
bool(true)
string(7) "message"
array(1) {
[0]=>
string(7) "runtime"
}

@ -0,0 +1,47 @@
--TEST--
Runtime property defaults bypass hooks and asymmetric write handlers
--FILE--
<?php
class HookRuntimeDefault
{
private int $writes = 0;
public array $values = ['initial'] {
get => $this->values;
set {
++$this->writes;
$this->values = $value;
}
}
public function writes(): int
{
return $this->writes;
}
}
class AsymmetricRuntimeDefault
{
public private(set) array $values = ['initial'];
}
function main(): void
{
$hooked = new HookRuntimeDefault();
$asymmetric = new AsymmetricRuntimeDefault();
var_dump($hooked->values, $hooked->writes());
var_dump($asymmetric->values);
}
?>
--EXPECT--
array(1) {
[0]=>
string(7) "initial"
}
int(0)
array(1) {
[0]=>
string(7) "initial"
}

@ -0,0 +1,44 @@
--TEST--
Known array statement writes preserve references, keys and expression results
--FILE--
<?php
declare(strict_types=1);
function main(): void
{
$array = [1];
$reference =& $array[0];
$array[0] = 3;
var_dump($reference);
$array[] = 4;
unset($array[1]);
$array[] = 5;
var_dump(array_keys($array), $array);
$array[0] += 7;
var_dump($reference);
$assigned = ($array[3] = 9);
$compound = ($array[0] += 1);
var_dump($assigned, $compound);
}
?>
--EXPECT--
int(3)
array(2) {
[0]=>
int(0)
[1]=>
int(2)
}
array(2) {
[0]=>
&int(3)
[2]=>
int(5)
}
int(10)
int(9)
int(11)
Loading…
Cancel
Save