- 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 efficientmaster
parent
7418b6247e
commit
cb1e12f11f
28 changed files with 1212 additions and 105 deletions
@ -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 测试组覆盖。 |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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]; |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue