diff --git a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md new file mode 100644 index 00000000..e58263cd --- /dev/null +++ b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md @@ -0,0 +1,131 @@ +# Native Class 实现验收矩阵 + +> 审计日期:2026-08-17 +> 本文记录 `#[Native]` 对象模型的需求、实现入口和直接验证证据。它是 +> [NATIVE_CLASS_OBJECT.md](NATIVE_CLASS_OBJECT.md) 的实现验收附件,不替代语义设计文档。 + +## 1. 验收原则 + +每一项能力必须同时具有: + +1. 明确的语言边界; +2. 可定位的编译器或 PHPX 实现; +3. 正向 PHPT、负向 PHPUnit 或 PHPX C++ 单测中的直接证据。 + +仅有代码、仅有文档或“当前没有发现失败”均不视为完成。Native Object 没有 Zend +表示,因此任何不能静态证明安全的跨边界行为都必须在生成 C++ 前拒绝。 + +## 2. 对象模型与代码生成 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| `#[Native]` 只用于具名 class | `NativeClassAttributeLowering`、`NativeClassSupportTrait` | `testRejectsNativeAttributeOnInterface/Trait/Enum/AnonymousClass` | 已验证 | +| 不注册 Zend class/object handlers | Native struct、descriptor 和自由函数生成路径 | `clone-and-zend-invisible.phpt`、Reflection 负向测试 | 已验证 | +| 方法保持 `php_*` 自由函数 ABI | Native method/virtual thunk 生成路径 | `basic.phpt`、`chained-call.phpt` | 已验证 | +| 静态可解析的 `new NativeClass()` 使用 Native Heap | `CompilerBase::parseNew()`、`php::nativeConstruct()` | `basic.phpt`、`construction-gc-roots.phpt` | 已验证 | +| `new (表达式)()` 保持普通 PHP 动态实例化 | `parseNew()` 只对 `Node\\Name` 进入 Native 分支 | `testLeavesDynamicClassExpressionsToTheOrdinaryPhpPath` | 已验证 | +| Native 对象本身不能充当动态 class target | `assertNotNativeObjectDynamicClassTarget()` | dynamic new/static call/class constant 负向测试 | 已验证 | +| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 128 项 `NativeClassValidationTest` | 已验证 | + +## 3. 属性与固定布局 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| 所有属性必须声明类型 | Native field validation | `testRejectsUntypedProperty` | 已验证 | +| bool/int/float 使用固定值字段 | Native field C++ type mapping | `basic.phpt`、`numeric-properties.phpt` | 已验证 | +| string/array/object/typed object/Stream/mixed 可作为字段 | Native PHPX field mapping、写入检查 | `phpx-properties.phpt`、`stream-property.phpt`、`composite-property-types.phpt` | 已验证 | +| BigInt/BigFloat/Decimal 可作为字段 | 高精度字段映射与 trace/destroy | `high-precision-properties.phpt` | 已验证 | +| Native 类型字段保存裸指针,可形成循环类型 | struct 前置声明、descriptor trace | `mutual-reference-types.phpt`、`gc-cycle.phpt` | 已验证 | +| 未显式初始化字段使用确定零值 | Native field initializer | `zero-values.phpt` | 已验证 | +| 属性写入保持声明类型 | Native property assignment validation | composite、stream 及多项负向 PHPUnit | 已验证 | +| 仅 `any` 属性允许取 PHP 引用 | Native property reference lowering | `any-property-reference.phpt` 及 mixed/fixed property 负向测试 | 已验证 | +| Native 属性不支持 `unset()` | property unset validator | `testRejectsUnsetOnNativeObjectProperties` | 已验证 | +| readonly 属性不支持 | Native declaration validator | `testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented` | 已验证 | +| Box/Std Container 不能嵌入字段 | Native field validator | Box/Std Container property 负向测试 | 已验证 | + +## 4. 身份、空值与调用 ABI + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| `$a = $b` 只复制指针并共享对象身份 | Native pointer local representation | `parameter-semantics.phpt` | 已验证 | +| Native 参数和返回必须显式声明具体类 | call argument/return boundary validation | untyped/mixed/interface 参数与返回负向测试 | 已验证 | +| 普通 Native 参数非空,`?Class` 才可为空 | function entry/return checks | `non-null-parameter.phpt`、`nullable-signatures.phpt`、`return-nullability.phpt` | 已验证 | +| Native 参数、返回和变量禁止 `&` | reference boundary validation | reference parameter/return/assignment/function/method 负向测试 | 已验证 | +| Native variadic、union/intersection signature 不支持 | signature validation | variadic/union/null-union 负向测试 | 已验证 | +| `unset($object)`/`$object = null` 只清当前 pointer slot | Native root slot lowering | `unset-alias.phpt` | 已验证 | +| `===`/`!==` 与 `match` 使用指针身份 | Native identity lowering | `strict-identity.phpt`、`match-identity.phpt` | 已验证 | +| ternary/match/coalesce 为兄弟子类选择最近公共 Native 基类 | `getCommonNativeObjectClass()`、selection pointer cast | `value-selection.phpt`、跨文件 global discovery 测试 | 已验证 | +| 条件表达式检查非空指针,不调用 `toBool()` | Native condition lowering | `conditions.phpt` | 已验证 | +| 松散比较、算术、位运算、增减、复合写入和 switch 禁止 | operator validators | 对应 PHPUnit 负向测试 | 已验证 | +| `isset`/`empty`/`is_null`/nullsafe 保持 typed pointer | Native selection/nullsafe lowering | `isset-empty.phpt`、`is-null.phpt`、`nullsafe.phpt` | 已验证 | +| 调用参数严格从左到右求值并在 safe point 精确 rooting | Native call argument materialization | `call-argument-roots.phpt`、`constructor-argument-roots.phpt` | 已验证 | + +## 5. 类语言能力 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| 单继承、abstract 与有限虚分派 | Native C++ inheritance/virtual slot adapters | `abstract-method.phpt`、`polymorphic-clone.phpt`、`virtual-signature-variance.phpt` | 已验证 | +| public/private/protected 在编译期检查 | Native member resolution | `method-visibility.phpt` 及不可访问方法/常量负向测试 | 已验证 | +| Trait 在注入后按普通 Native member 编译 | 现有 Trait AST 注入 + Native member generation | `trait-inheritance-interface.phpt` | 已验证 | +| Interface 仅作编译期契约,不能成为值表示 | interface contract validator | `internal-interface.phpt`、`interface-property-hooks.phpt` 及 interface escape 负向测试 | 已验证 | +| 编译期可解析的 `instanceof` 折叠 | Native instanceof lowering | `instanceof.phpt`、dynamic instanceof 负向测试 | 已验证 | +| Getter/Setter 注解生成直接调用 | annotation lowering + Native method path | `generators.phpt` | 已验证 | +| Property Hook 只支持直接 get/set | Native hook lowering | `property-hooks.phpt`、`property-hook-native-object.phpt` 及间接操作负向测试 | 已验证 | +| `clone` 保持动态子类、PHPX COW 和浅对象语义 | Native clone descriptor/thunk、`php::nativeClone()` | clone 系列 PHPT、`clone-phpx-fields.phpt` | 已验证 | +| `__construct` 仅由 `new` 调用 | Native construction path、显式调用检查 | construction 系列 PHPT、explicit constructor 负向测试 | 已验证 | +| `__destruct` 由 GC 至多执行一次,继承链 derived-to-base | Native finalizer chain | destructor/finalizer/lifecycle 系列 PHPT | 已验证 | +| `__invoke` 和 `__toString` 使用确定 Native Call | Native magic method allow-list | `magic-methods.phpt` | 已验证 | +| 动态魔术方法、变量属性/方法名不支持 | Native magic/dynamic access deny-list | dynamic magic、variable method/property 负向测试 | 已验证 | +| `toArray/toString/toInt/toFloat/toBool` 要求实体方法和精确返回类型 | Native keyword method resolution | `keyword-conversions.phpt` 及 missing/wrong return 负向测试 | 已验证 | +| `count($obj)` 仅在实现 Countable 时特化 | Native count optimizer | `keyword-conversions.phpt`、count-without-countable 负向测试 | 已验证 | + +## 6. GC 与生命周期 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| Wren 风格精确、非移动、STW mark-sweep | `phpx/thirdparty/wren-gc`、`native_gc.cc` | PHPX `wren_gc.*` | 已验证 | +| 裸指针写入无 RC、无 write barrier | Native pointer field/local codegen | 生成 C++ 审查、Native PHPT | 已验证 | +| 10 MiB 初始阈值、1 MiB 下限、50% headroom | Wren GC 配置 | `wren_gc.uses_stable_native_heap_defaults` | 已验证 | +| 精确 root frame 保持对象图存活 | `NativeRootFrame`、generated root slots | PHPX root tests、`gc-cycle.phpt` | 已验证 | +| Fiber 非 LIFO 生命周期安全 | root frame registry | `fiber-lifetime.phpt`、`fiber-shutdown.phpt`、PHPX Fiber root tests | 已验证 | +| global/static request roots 在 ZTS 下为 thread-local | generated globals/root registration | ZTS 环境下 `global-and-static.phpt`、PHPX request root tests | 已验证 | +| RSHUTDOWN 清空 root 并销毁 heap | `nativeGcRequestShutdown()` | PHPX shutdown tests | 已验证 | +| finalizer 可复活一次,之后不重复执行 | Wren/Native finalization state | `gc-cycle.phpt`、PHPX resurrection tests | 已验证 | +| finalizer 中分配、异常和 Zend 状态安全 | finalizer queue/exception cleanup | finalizer/lifecycle PHPT、PHPX finalizer tests | 已验证 | +| 构造或克隆失败不产生悬空对象,已逃逸对象保持有效 | `nativeConstruct()`、`nativeClone()` failure paths | `failed-lifecycle-escape.phpt`、`failed-clone-finalizer.phpt` | 已验证 | + +## 7. ZendVM 边界与容器 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| Native Object 不能进入 PHP array/object property/mixed | escape and boundary validators | 对应 PHPUnit 负向测试 | 已验证 | +| 不能传给 PHP/ZendVM 动态函数、Closure 或 constructor | call boundary validator | dynamic call、Closure、Zend constructor 负向测试 | 已验证 | +| Reflection/WeakReference/serialize/json_encode 不支持 | facility-specific diagnostics | 对应 PHPUnit 负向测试 | 已验证 | +| Generator 不能保存、接收或产出 Native pointer | generator boundary validator | generator 系列负向测试 | 已验证 | +| 普通函数跨 Fiber suspend 的 Native local 有精确 root | root frame lifecycle | Fiber PHPT | 已验证 | +| 局部 Std Container 可保存具体 Native pointer | Std Container Native value mapping/root frame | `std-containers.phpt` | 已验证 | +| Native Std Container 不能逃逸为 Zend 值、static/global 或 closure capture | container escape validation | Std Container 系列负向 PHPUnit | 已验证 | +| `include`/`eval` 不暴露 Native local 到 Zend symbol table | include scope filtering | `include-native-scope.phpt` | 已验证 | + +## 8. 项目级分析 + +| 要求 | 实现证据 | 测试证据 | 结论 | +|---|---|---|---| +| Native class 前向声明不依赖文件顺序 | declaration discovery pre-pass | `testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing` | 已验证 | +| global Native slot ABI 在任一 C++ 文件生成前确定 | `NativeGlobalDiscovery`、`NativeGlobalTypeResolver` | `testDiscoversNativeGlobalSlotBeforeEarlierReaderIsConverted`,实际双文件构建 | 已验证 | +| `global $slot` 与字面量 `$GLOBALS['slot']` 使用同一 Native root slot | literal global slot lowering、request root registration | `global-and-static.phpt`、跨文件 `$GLOBALS` fixture | 已验证 | +| global slot 固定首个 Native 类型,只允许子类或 null | global registration/type validation | `global-and-static.phpt`、global type change 负向测试 | 已验证 | +| 未使用 Native Class 的项目跳过 Native global pre-pass | `discoverNativeGlobalObjects()` fast return | 源码检查、全量 PHPUnit | 已验证 | + +## 9. 当前验证命令 + +```bash +./run-tests.php -j4 --compiler ./tpc tests/compiler/native-class/ +vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php +/home/swoole/workspace/aot/phpx/build/bin/phpx-tests \ + --gtest_filter='wren_gc.*:native_gc.*' +``` + +本次结果分别为:69/69 PHPT、128/128 PHPUnit、17/17 PHPX C++ tests。 +最终合入前仍需执行编译器完整 PHPUnit、完整 PHPT 和 PHPX 完整测试,防止 Native +分支的公共 hook 影响普通对象模型。 diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 483926f5..b5f499af 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -1,8 +1,9 @@ # Native Class Object 设计与实现 -> 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、 -> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派和 -> Interface 编译期契约已经落地;本文同时记录尚未开放的边界。 +> 状态:核心方案已实现,正在进行最终整体验证。固定布局、Native Call、精确 tracing GC、 +> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派、 +> Interface 编译期契约及项目级 global slot 预发现均已落地。逐项实现证据见 +> [NATIVE_CLASS_IMPLEMENTATION_AUDIT.md](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。 ## 1. 背景 @@ -46,7 +47,7 @@ Native Class Object 初版不追求以下能力: ## 4. 显式声明 -建议使用专用注解,暂定为: +使用专用内置注解: ```php #[Native] @@ -1292,49 +1293,43 @@ $json = json_encode($nativeObject->toArray()); | Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 | | late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 | -## 17. 编译器目录与隔离要求 +## 17. 实际目录与隔离方式 -Native Class 的实现应集中放置在独立目录: +当前实现把对象模型的主体规则集中在以下位置: ```text src/NativeClass/ -├── Analysis/ -│ ├── NativeClassAnalyzer.php -│ ├── NativeEscapeAnalyzer.php -│ ├── NativeBoundaryValidator.php -│ └── NativeRootAnalyzer.php -├── CodeGen/ -│ ├── NativeClassGenerator.php -│ ├── NativeMethodGenerator.php -│ ├── NativePropertyGenerator.php -│ ├── NativeTraceGenerator.php -│ └── NativeRootFrameGenerator.php -├── Model/ -│ ├── NativeClassDefinition.php -│ ├── NativeFieldDefinition.php -│ └── NativeMethodDefinition.php -├── Transform/ -│ ├── NativeAnnotationExpander.php -│ ├── NativeCloneLowering.php -│ └── NativePropertyHookLowering.php -├── Diagnostics/ -│ └── NativeClassDiagnostic.php -└── NativeClassCompiler.php +├── NativeClassSupportTrait.php # 声明、布局、方法、边界和 codegen 策略 +├── NativeGlobalDiscovery.php # 项目级 Native global slot 预发现 +└── NativeGlobalTypeResolver.php # 预发现器的只读符号查询边界 + +src/Transform/NativeClassAttributeLowering.php +src/TypeSystem/NativeTypeCompatibilityTrait.php + +phpx/include/phpx_native_gc.h +phpx/src/core/native_gc.cc +phpx/thirdparty/wren-gc/ ``` -隔离原则: +普通 parser、call generator、property resolver 和 control-flow lowering 中只保留进入 +Native 策略所需的窄 hook。Native 具体诊断、类型映射、字段生成、virtual thunk、trace、 +clone 和 finalizer 规则集中在 `NativeClassSupportTrait`;项目级预分析放在同目录的独立 +analyzer 中。这样可以复用 TypePHP 已有 AST、符号表和求值顺序基础设施,而不复制一套 +容易发生语义漂移的平行编译器。 -1. 现有普通 class 编译流程不得包含 Native Class 的具体规则。 -2. 公共编译流程只负责识别显式标记,并将完整 class AST 交给 `NativeClassCompiler`。 -3. Native Class 的类型信息优先保存在独立 side table 中,不向现有 Class Definition 持续增加特殊状态字段。 -4. Native Class 的边界检查、逃逸分析、Hook lowering 和 C++ 生成都在该目录内完成。 -5. 未使用 Native Class 的项目不加载相关分析器,不增加普通编译流程的运行成本。 -6. Native Class 需要的运行时支持应放入独立头文件,且只在实际使用时 include。 +隔离约束如下: -建议对应测试目录: +1. 普通对象路径不得生成 Native pointer,也不得依赖 Native Heap。 +2. 公共 hook 必须先检查确定的 Native 类型;未命中时保持原有路径。 +3. 项目中没有 Native class 时,global pre-pass 在扫描源码前立即返回。 +4. Native Object 不能通过 fallback 进入 `php::Var`、Zend Object 或动态调用。 +5. GC runtime 位于独立 PHPX 头文件和源文件中;第三方 Wren 派生代码保留来源与 MIT + license 文件。 +6. Native 正向测试与编译期拒绝测试分别集中在: ```text tests/compiler/native-class/ +phpunit/src/NativeClass/NativeClassValidationTest.php ``` 普通 class 的测试与 Native Class 测试不得混用,以明确两套对象模型的语义边界。 @@ -1376,21 +1371,24 @@ Native Class 的主要路径必须满足: 若某个 PHP 特性无法满足这些要求,应优先禁止该特性,而不是降低所有 Native Class 的性能。 -## 20. 建议的实施阶段 +## 20. 实施状态 -实现按以下顺序推进: +已落地的实施阶段: -1. 固定 `#[Native] class` 语法、typed object 规则和诊断边界。 -2. 建立独立 AST model、side table 和 C++ struct 生成器。 -3. 建立 Native Heap、精确 tracing GC、root frame、循环回收和异常清理机制。 -4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。 -5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。 -6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。 -7. 支持 Property Hook 的直接 getter/setter lowering;复合写入等动态语义编译期拒绝。 -8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。 -9. 最后评估 `json_encode()`、栈分配与逃逸分析。 +1. `#[Native] class` 语法、typed pointer 规则和编译期诊断边界。 +2. C++ struct、固定字段、descriptor、trace 和 `php_*` 方法生成。 +3. Wren 风格 Native Heap、精确 root frame、循环回收、异常恢复和 request shutdown。 +4. 构造、析构、属性类型、PHPX 字段、普通方法及 typed pointer 参数/返回。 +5. Trait AST 注入、Getter/Setter 和关键词方法直接调用。 +6. 单继承、abstract、override virtual thunk、签名 variance 和 Interface 编译期契约。 +7. Property Hook 直接 getter/setter lowering,并拒绝所有间接与复合写入。 +8. clone、动态子类 clone、循环类型、生命周期失败和对象复活处理。 +9. Std Container 局部 Native value、Fiber root、global/static request root 与跨文件 slot + ABI 预发现。 -每个阶段都必须先添加 PHPT/PHPUnit 测试,再实现代码。 +`json_encode()` 已确定不支持直接接收 Native Object;使用显式 `toArray()` 边界。 +栈分配和逃逸分析仍属于独立的后续性能优化,不是当前对象模型正确性的组成部分。 +每一项已实现能力均同时具有 PHPT、PHPUnit 或 PHPX C++ 测试,详细对应关系见验收矩阵。 ## 21. 已确定但仍需性能验证的参数 diff --git a/phpunit/code/native-class-global-forward/a.php b/phpunit/code/native-class-global-forward/a.php index eec04844..c034bf44 100644 --- a/phpunit/code/native-class-global-forward/a.php +++ b/phpunit/code/native-class-global-forward/a.php @@ -5,3 +5,20 @@ function readNativeForwardGlobal(): int global $nativeForwardGlobal; return $nativeForwardGlobal->value; } + +function readNativeForwardPolymorphic(): int +{ + global $nativeForwardPolymorphic; + return $nativeForwardPolymorphic->value; +} + +function readNativeForwardCoalesced(): int +{ + global $nativeForwardCoalesced; + return $nativeForwardCoalesced->value; +} + +function readNativeForwardGlobalsArray(): int +{ + return $GLOBALS['nativeForwardGlobalsArray']->value; +} diff --git a/phpunit/code/native-class-global-forward/b.php b/phpunit/code/native-class-global-forward/b.php index e901b955..1fc318b4 100644 --- a/phpunit/code/native-class-global-forward/b.php +++ b/phpunit/code/native-class-global-forward/b.php @@ -20,3 +20,54 @@ class NativeForwardGlobalFactory $nativeForwardGlobal = $local; } } + +#[Native] +class NativeForwardGlobalCarrier +{ + public NativeForwardGlobalValue $value; + + public function create(): NativeForwardGlobalValue + { + return new NativeForwardGlobalValue(); + } +} + +function initializeNativeForwardGlobalFromCarrier(NativeForwardGlobalCarrier $carrier): void +{ + global $nativeForwardGlobal; + $nativeForwardGlobal = $carrier->create(); + $nativeForwardGlobal = $carrier->value; +} + +#[Native] +class NativeForwardBase +{ + public int $value = 1; +} + +#[Native] +class NativeForwardLeft extends NativeForwardBase +{ +} + +#[Native] +class NativeForwardRight extends NativeForwardBase +{ +} + +function initializeNativeForwardPolymorphic(bool $left): void +{ + global $nativeForwardPolymorphic; + $nativeForwardPolymorphic = $left ? new NativeForwardLeft() : new NativeForwardRight(); +} + +function initializeNativeForwardCoalesced(): void +{ + global $nativeForwardCoalesced; + $nativeForwardCoalesced ??= new NativeForwardGlobalValue(); +} + +function initializeNativeForwardGlobalsArray(): void +{ + $GLOBALS['nativeForwardGlobalsArray'] = new NativeForwardGlobalValue(); +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index 6149b2f6..7b91ee14 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -47,6 +47,22 @@ final class NativeClassValidationTest extends \BaseTest 'php::nativeDeref(nativeForwardGlobal, "NativeForwardGlobalValue").value', $code, ); + self::assertStringContainsString( + 'php::nativeDeref(nativeForwardPolymorphic, "NativeForwardBase").value', + $code, + ); + self::assertStringContainsString( + 'php::nativeDeref(nativeForwardCoalesced, "NativeForwardGlobalValue").value', + $code, + ); + self::assertStringContainsString( + 'auto &nativeForwardGlobalsArray = _global_var_nativeForwardGlobalsArray;', + $code, + ); + self::assertMatchesRegularExpression( + '/php::nativeDeref\\(tmp_var_\\d+, "NativeForwardGlobalValue"\\)\\.value/', + $code, + ); self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code); } diff --git a/run-tests.php b/run-tests.php index b561acce..acd09311 100755 --- a/run-tests.php +++ b/run-tests.php @@ -39,6 +39,8 @@ Options: -j, -j , --job Run up to simultaneous testing processes in parallel for quicker testing on systems with multiple logical processors. + When more than one test worker is enabled, each tpc process + compiles with one job to avoid multiplying both parallelism levels. -l Read the testfiles to be executed from . After the test has finished all failed tests are written to the same . @@ -4439,6 +4441,7 @@ function compile_wasm_php_file(string $file, string $profile, string $compilerAr if ($compilerArgs !== '') { array_push($command, ...parse_wasm_test_args($compilerArgs)); } + array_push($command, ...get_aot_compiler_job_args()); $log = $root . DIRECTORY_SEPARATOR . 'compile.log'; $process = proc_open( @@ -4576,6 +4579,9 @@ function compile_php_file(string $file, string $compiler_args = ''): string if ($compiler_args !== '') { $cmd .= ' ' . $compiler_args; } + foreach (get_aot_compiler_job_args() as $argument) { + $compiler_output_args .= ' ' . escapeshellarg($argument); + } exec($cmd . $compiler_output_args . ' 2>&1', $output, $exitCode); clearstatcache(true, $binary_file); @@ -4586,6 +4592,23 @@ function compile_php_file(string $file, string $compiler_args = ''): string return $binary_file; } +/** + * Keep test-level and compiler-level parallelism from multiplying each other. + * + * `parse_worker_count()` represents the sequential `-j1` mode as null, so a + * non-null value here means run-tests owns more than one concurrent test slot. + * Append this after --AOT_ARGS-- so an individual test cannot accidentally + * re-enable nested compiler parallelism. + * + * @return list + */ +function get_aot_compiler_job_args(): array +{ + global $workers; + + return $workers !== null && $workers > 1 ? ['--job', '1'] : []; +} + function create_aot_parallel_root(): string { $suffix = getmypid() . '-' . bin2hex(random_bytes(6)); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 3ea4691e..a64eddbd 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1974,6 +1974,10 @@ class CompilerBase implements PropertyAccessContext return $this->getObjectType($object); } } + $globalSlot = $this->getLiteralGlobalsSlot($expr); + if ($globalSlot !== null && isset($this->nativeGlobalObjects[$globalSlot])) { + return $this->nativeGlobalObjects[$globalSlot]; + } if ($expr instanceof Expr\PropertyFetch && $this->isIdExpr($expr->name)) { $receiverClass = $this->detectClassOfExpr($expr->var); if ($this->isNativeObjectClass($receiverClass)) { diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index fe35b2e6..8778efcf 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -228,7 +228,17 @@ trait NativeClassSupportTrait )); } - /** @var \ReflectionNamedType $type */ + if (!$type instanceof \ReflectionNamedType) { + throw new \LogicException('Unsupported reflection type: ' . $type::class); + } + return $this->reflectionNamedTypeToNode($type, $declaringClass, $allowNullableWrapper); + } + + private function reflectionNamedTypeToNode( + \ReflectionNamedType $type, + \ReflectionClass $declaringClass, + bool $allowNullableWrapper, + ): NodeAbstract { $name = $type->getName(); $lower = strtolower($name); if ($lower === 'self') { @@ -365,6 +375,18 @@ trait NativeClassSupportTrait } } + protected function getLiteralGlobalsSlot(NodeAbstract $expression): ?string + { + if (!$expression instanceof Node\Expr\ArrayDimFetch + || !$expression->var instanceof Node\Expr\Variable + || $expression->var->name !== 'GLOBALS' + || !$expression->dim instanceof Node\Scalar\String_ + ) { + return null; + } + return $expression->dim->value; + } + protected function getNativeObjectCppName(string|ClassDef $class): string { if ($class instanceof ClassDef) { @@ -523,18 +545,36 @@ trait NativeClassSupportTrait $common = $class; continue; } - if ($this->isObjectClassStaticallyAssignableTo($class, $common)) { - continue; - } - if ($this->isObjectClassStaticallyAssignableTo($common, $class)) { - $common = $class; - continue; + $common = $this->getCommonNativeObjectClass($common, $class); + if ($common === '') { + return ''; } - return ''; } return $common; } + protected function getCommonNativeObjectClass(string $left, string $right): string + { + $leftAncestors = []; + while ($this->isNativeObjectClass($left)) { + $definition = $this->getClass($left); + $canonical = $definition->getNamespacedName(false); + $leftAncestors[strtolower($canonical)] = $canonical; + $left = $definition->extends; + } + + while ($this->isNativeObjectClass($right)) { + $definition = $this->getClass($right); + $canonical = $definition->getNamespacedName(false); + $key = strtolower($canonical); + if (isset($leftAncestors[$key])) { + return $leftAncestors[$key]; + } + $right = $definition->extends; + } + return ''; + } + protected function assertSupportedNativeObjectTypeNode( ?NodeAbstract $type, int $declarationKind, diff --git a/src/NativeClass/NativeGlobalDiscovery.php b/src/NativeClass/NativeGlobalDiscovery.php index 10624e80..9fe1a05e 100644 --- a/src/NativeClass/NativeGlobalDiscovery.php +++ b/src/NativeClass/NativeGlobalDiscovery.php @@ -1,6 +1,6 @@ ): ?string */ - private \Closure $functionReturn; - - /** @var \Closure(string, string): ?string */ - private \Closure $methodReturn; - - /** @var \Closure(string, string): ?string */ - private \Closure $propertyClass; - - /** @var \Closure(string, string): ?string */ - private \Closure $commonClass; - - /** @var \Closure(string): ?string */ - private \Closure $parentClass; + private NativeGlobalTypeResolver $resolver; + /** @var array */ + private array $functionReturns; private string $scopeClass = ''; /** - * @param \Closure(string): ?string $canonicalClass - * @param \Closure(string): ?string $nativeClass - * @param \Closure(list): ?string $functionReturn - * @param \Closure(string, string): ?string $methodReturn - * @param \Closure(string, string): ?string $propertyClass - * @param \Closure(string, string): ?string $commonClass - * @param \Closure(string): ?string $parentClass + * @param array $functionReturns */ public function __construct( - \Closure $canonicalClass, - \Closure $nativeClass, - \Closure $functionReturn, - \Closure $methodReturn, - \Closure $propertyClass, - \Closure $commonClass, - \Closure $parentClass, + NativeGlobalTypeResolver $resolver, + array $functionReturns, ) { - $this->canonicalClass = $canonicalClass; - $this->nativeClass = $nativeClass; - $this->functionReturn = $functionReturn; - $this->methodReturn = $methodReturn; - $this->propertyClass = $propertyClass; - $this->commonClass = $commonClass; - $this->parentClass = $parentClass; + $this->resolver = $resolver; + $this->functionReturns = $functionReturns; } /** @@ -129,7 +96,7 @@ final class NativeGlobalDiscovery $locals = []; $previousScope = $this->scopeClass; - $thisClass = ($this->canonicalClass)($class); + $thisClass = $this->resolver->canonicalClass($class); $this->scopeClass = $thisClass ?? ''; if ($thisClass !== null) { $locals['this'] = $thisClass; @@ -226,11 +193,11 @@ final class NativeGlobalDiscovery if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { return; } - if ($node instanceof Node\Expr\Assign) { + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp\Coalesce) { $this->analyzeNodes($node->expr, $globals, $locals, $result); $class = $this->inferClass($node->expr, $locals); $globalName = $this->assignmentGlobalName($node->var, $globals); - $nativeClass = $class === null ? null : ($this->nativeClass)($class); + $nativeClass = $class === null ? null : $this->resolver->nativeClass($class); if ($globalName !== null && $nativeClass !== null) { $result[] = ['name' => $globalName, 'class' => $nativeClass, 'node' => $node]; } elseif ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)) { @@ -251,7 +218,7 @@ final class NativeGlobalDiscovery /** @param array $locals */ private function inferClass(NodeAbstract $expression, array $locals): ?string { - if ($expression instanceof Node\Expr\Assign) { + if ($expression instanceof Node\Expr\Assign || $expression instanceof Node\Expr\AssignOp\Coalesce) { return $this->inferClass($expression->expr, $locals); } if ($expression instanceof Node\Expr\New_ && $expression->class instanceof Node\Name) { @@ -266,26 +233,38 @@ final class NativeGlobalDiscovery return $this->inferClass($expression->expr, $locals); } if ($expression instanceof Node\Expr\FuncCall && $expression->name instanceof Node\Name) { - return ($this->functionReturn)($this->resolvedNameCandidates($expression->name)); + foreach ($this->resolvedNameCandidates($expression->name) as $name) { + $key = strtolower(ltrim($name, '\\')); + if (isset($this->functionReturns[$key])) { + return $this->functionReturns[$key]; + } + } + return null; } if ($expression instanceof Node\Expr\StaticCall && $expression->class instanceof Node\Name && $expression->name instanceof Node\Identifier ) { $class = $this->resolvedClass($expression->class); - return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString()); + return $class === null + ? null + : $this->resolver->methodReturn($class, $expression->name->toString()); } if ($expression instanceof Node\Expr\MethodCall && $expression->name instanceof Node\Identifier ) { $class = $this->inferClass($expression->var, $locals); - return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString()); + return $class === null + ? null + : $this->resolver->methodReturn($class, $expression->name->toString()); } if ($expression instanceof Node\Expr\PropertyFetch && $expression->name instanceof Node\Identifier ) { $class = $this->inferClass($expression->var, $locals); - return $class === null ? null : ($this->propertyClass)($class, $expression->name->toString()); + return $class === null + ? null + : $this->resolver->propertyClass($class, $expression->name->toString()); } if ($expression instanceof Node\Expr\Ternary) { $if = $expression->if === null @@ -317,7 +296,7 @@ final class NativeGlobalDiscovery if ($right === null) { return $left; } - return ($this->commonClass)($left, $right); + return $this->resolver->commonClass($left, $right); } private function classFromType(?NodeAbstract $type): ?string @@ -373,9 +352,11 @@ final class NativeGlobalDiscovery return $this->scopeClass !== '' ? $this->scopeClass : null; } if ($keyword === 'parent') { - return $this->scopeClass !== '' ? ($this->parentClass)($this->scopeClass) : null; + return $this->scopeClass !== '' + ? $this->resolver->parentClass($this->scopeClass) + : null; } - return ($this->canonicalClass)($this->resolvedName($name)); + return $this->resolver->canonicalClass($this->resolvedName($name)); } /** @return list */ diff --git a/src/NativeClass/NativeGlobalTypeResolver.php b/src/NativeClass/NativeGlobalTypeResolver.php new file mode 100644 index 00000000..5cde7961 --- /dev/null +++ b/src/NativeClass/NativeGlobalTypeResolver.php @@ -0,0 +1,147 @@ + */ + private array $classes = []; + + /** @var array */ + private array $nativeClasses = []; + + /** @var array */ + private array $parents = []; + + /** @var array> */ + private array $methodReturns = []; + + /** @var array> */ + private array $propertyClasses = []; + + /** @param array $classes */ + public function __construct(array $classes) + { + foreach ($classes as $class) { + $name = $class->getNamespacedName(false); + $key = strtolower(ltrim($name, '\\')); + $this->classes[$key] = $name; + if ($class->nativeObject) { + $this->nativeClasses[$key] = true; + } + } + + foreach ($classes as $class) { + $name = $class->getNamespacedName(false); + $key = strtolower(ltrim($name, '\\')); + $parent = $this->canonicalClass($class->extends); + if ($parent !== null) { + $this->parents[$key] = $parent; + } + + foreach ($class->methods as $method => $definition) { + $this->methodReturns[$key][$method] + = $this->canonicalClass($definition->functionDef->returnClass); + } + foreach ($class->abstractMethodDefs as $method => $definition) { + $this->methodReturns[$key][$method] + = $this->canonicalClass($definition->functionDef->returnClass); + } + foreach ($class->properties as $property => $definition) { + $this->propertyClasses[$key][$property] + = $this->canonicalClass($definition->class); + } + } + } + + public function canonicalClass(string $class): ?string + { + return $this->classes[strtolower(ltrim($class, '\\'))] ?? null; + } + + public function nativeClass(string $class): ?string + { + $key = strtolower(ltrim($class, '\\')); + return isset($this->nativeClasses[$key]) ? $this->classes[$key] : null; + } + + public function methodReturn(string $class, string $method): ?string + { + $key = strtolower(ltrim($class, '\\')); + $method = strtolower($method); + while (isset($this->classes[$key])) { + if (array_key_exists($method, $this->methodReturns[$key] ?? [])) { + return $this->methodReturns[$key][$method]; + } + $parent = $this->parents[$key] ?? null; + if ($parent === null) { + return null; + } + $key = strtolower($parent); + } + return null; + } + + public function propertyClass(string $class, string $property): ?string + { + $key = strtolower(ltrim($class, '\\')); + while (isset($this->classes[$key])) { + if (array_key_exists($property, $this->propertyClasses[$key] ?? [])) { + return $this->propertyClasses[$key][$property]; + } + $parent = $this->parents[$key] ?? null; + if ($parent === null) { + return null; + } + $key = strtolower($parent); + } + return null; + } + + public function commonClass(string $left, string $right): ?string + { + $leftKey = strtolower(ltrim($left, '\\')); + $rightKey = strtolower(ltrim($right, '\\')); + $leftAncestors = []; + while (isset($this->classes[$leftKey])) { + $leftAncestors[$leftKey] = true; + $parent = $this->parents[$leftKey] ?? null; + if ($parent === null) { + break; + } + $leftKey = strtolower($parent); + } + + while (isset($this->classes[$rightKey])) { + if (isset($leftAncestors[$rightKey])) { + return $this->classes[$rightKey]; + } + $parent = $this->parents[$rightKey] ?? null; + if ($parent === null) { + break; + } + $rightKey = strtolower($parent); + } + return null; + } + + public function parentClass(string $class): ?string + { + return $this->parents[strtolower(ltrim($class, '\\'))] ?? null; + } +} diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 0837d9a1..80954c70 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -96,7 +96,10 @@ trait ArrayExpressionTrait $this->addGlobalVar($name, Type::VAR); } if (!$this->hasScopeGlobalVar($name)) { - $this->addScopeGlobalVar($name, Type::VAR); + $this->addScopeGlobalVar($name, $this->globalVars[$name]); + } + if (isset($this->nativeGlobalObjects[$name])) { + $this->addNativeObject($name, $this->nativeGlobalObjects[$name]); } return $name; } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index ac534f83..21d33206 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -32,9 +32,25 @@ trait AssignOpTrait } if ($this->isVarExpr($left->var) && $left->var->name === 'GLOBALS') { $target = $this->parseGlobalsArrayDimFetch($left); - $value = $this->parseExprAsValue($right); $tmp = $this->genTmpVarName(); - $this->addLocalVar($tmp, Type::VAR); + if ($this->isNativeObjectVar($target)) { + $targetClass = $this->getNativeObjectVarClass($target); + $rightClass = $this->detectClassOfExpr($right); + if ($this->isNull($right)) { + $value = 'nullptr'; + } elseif (!$this->isNativeObjectClass($rightClass)) { + $this->fatalError($right, "Native object `\${$target}` cannot be converted to var/object"); + } elseif (!$this->isObjectClassStaticallyAssignableTo($rightClass, $targetClass)) { + $this->fatalError($right, "Cannot assign native object `{$rightClass}` to `{$targetClass}`"); + } else { + $value = $this->parseExprAsValue($right); + } + $this->addLocalVar($tmp, $this->getNativeObjectPointerType($targetClass)); + $this->addNativeObject($tmp, $targetClass); + } else { + $value = $this->parseExprAsValue($right); + $this->addLocalVar($tmp, Type::VAR); + } return '((' . $tmp . ' = ' . $value . ', ' . $target . ' = ' . $tmp . '), ' . $tmp . ')'; } $array = $this->parseWritableIdentifier($left->var); @@ -259,6 +275,15 @@ trait AssignOpTrait if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); } + } elseif (($globalSlot = $this->getLiteralGlobalsSlot($left)) !== null) { + if (!$this->hasGlobalVar($globalSlot)) { + $this->addGlobalVar($globalSlot, Type::VAR); + } + if (!$this->hasScopeGlobalVar($globalSlot)) { + $this->addScopeGlobalVar($globalSlot, $this->globalVars[$globalSlot]); + } + $this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $right); + $allowed = true; } elseif ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) { $receiverClass = $this->detectClassOfExpr($left->var); if ($this->isNativeObjectClass($receiverClass)) { @@ -1164,6 +1189,19 @@ trait AssignOpTrait // declared as Variant because boxing the raw pointer would coerce it // to bool. Other values retain the normal nullable Variant behavior. $var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; + $globalSlot = $this->getLiteralGlobalsSlot($expr->var); + if ($globalSlot !== null) { + if (!$this->hasGlobalVar($globalSlot)) { + $this->addGlobalVar($globalSlot, Type::VAR); + } + if (!$this->hasScopeGlobalVar($globalSlot)) { + $this->addScopeGlobalVar($globalSlot, $this->globalVars[$globalSlot]); + } + if ($nativeRight) { + $this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $expr->expr); + } + $var = $globalSlot; + } if ($var !== null && !$this->hasVar($var)) { if ($nativeRight) { $this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); @@ -1186,7 +1224,9 @@ trait AssignOpTrait } } - $isset = $this->parseChainedExpr($expr->var, self::OP_ISSET); + $isset = $var !== null && $this->isNativeObjectVar($var) + ? $var . ' != nullptr' + : $this->parseChainedExpr($expr->var, self::OP_ISSET); $var ??= $this->parseWritableIdentifier($expr->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index 29ff0afb..8a4c0fbb 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -45,11 +45,16 @@ trait SelectionExpressionTrait $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount); if ($nativeSelection) { + $pointerType = $this->getNativeObjectPointerType($nativeClass); if ($this->isNull($expr->if)) { $if = 'nullptr'; + } else { + $if = 'static_cast<' . $pointerType . '>(' . $if . ')'; } if ($this->isNull($expr->else)) { $else = 'nullptr'; + } else { + $else = 'static_cast<' . $pointerType . '>(' . $else . ')'; } } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 0aedde23..06206776 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -31,6 +31,7 @@ use TypePhp\Transform\ConstantExpressionValidationVisitor; use TypePhp\Transform\RuntimeAttributeFactoryLowering; use TypePhp\Transform\Visitor; use TypePhp\NativeClass\NativeGlobalDiscovery; +use TypePhp\NativeClass\NativeGlobalTypeResolver; use PhpParser\Modifiers; use PhpParser\ConstExprEvaluator; use PhpParser\Node; @@ -126,6 +127,22 @@ class Preprocessor extends CompilerBase return; } + $candidateSources = []; + foreach ($files as $file) { + if (!$this->isPhpFileForNativeDiscovery($file)) { + continue; + } + $source = file_get_contents($file); + if (is_string($source) + && (str_contains($source, 'global') || str_contains($source, '$GLOBALS')) + ) { + $candidateSources[$file] = $source; + } + } + if ($candidateSources === []) { + return; + } + $functionReturns = []; foreach ($this->symbols->functions() as $function) { if (!$function->method && $this->hasClass($function->returnClass)) { @@ -134,86 +151,10 @@ class Preprocessor extends CompilerBase } } - $discovery = new NativeGlobalDiscovery( - function (string $class): ?string { - $class = ltrim($class, '\\'); - if (!$this->hasClass($class)) { - return null; - } - return $this->getClass($class)->getNamespacedName(false); - }, - function (string $class): ?string { - $class = ltrim($class, '\\'); - if (!$this->isNativeObjectClass($class)) { - return null; - } - return $this->getClass($class)->getNamespacedName(false); - }, - static function (array $names) use ($functionReturns): ?string { - foreach ($names as $name) { - $key = strtolower(ltrim($name, '\\')); - if (isset($functionReturns[$key])) { - return $functionReturns[$key]; - } - } - return null; - }, - function (string $class, string $method): ?string { - while ($this->hasClass($class)) { - $classDefinition = $this->getClass($class); - if ($classDefinition->hasMethod($method)) { - $returnClass = $classDefinition->getMethod($method)->functionDef->returnClass; - return $this->hasClass($returnClass) - ? $this->getClass($returnClass)->getNamespacedName(false) - : null; - } - $class = $classDefinition->extends; - } - return null; - }, - function (string $class, string $property): ?string { - while ($this->hasClass($class)) { - $classDefinition = $this->getClass($class); - if ($classDefinition->hasProperty($property)) { - $propertyClass = $classDefinition->getProperty($property)->class; - return $this->hasClass($propertyClass) - ? $this->getClass($propertyClass)->getNamespacedName(false) - : null; - } - $class = $classDefinition->extends; - } - return null; - }, - function (string $left, string $right): ?string { - if ($this->isObjectClassStaticallyAssignableTo($left, $right)) { - return $right; - } - if ($this->isObjectClassStaticallyAssignableTo($right, $left)) { - return $left; - } - return null; - }, - function (string $class): ?string { - if (!$this->hasClass($class)) { - return null; - } - $parent = $this->getClass($class)->extends; - return $this->hasClass($parent) - ? $this->getClass($parent)->getNamespacedName(false) - : null; - }, - ); + $resolver = new NativeGlobalTypeResolver($this->symbols->classes()); + $discovery = new NativeGlobalDiscovery($resolver, $functionReturns); - foreach ($files as $file) { - if (!$this->isPhpFileForNativeDiscovery($file)) { - continue; - } - $source = file_get_contents($file); - if (!is_string($source) - || (!str_contains($source, 'global') && !str_contains($source, '$GLOBALS')) - ) { - continue; - } + foreach ($candidateSources as $source) { try { $ast = $this->parser->parse($source); } catch (\PhpParser\Error) { @@ -228,12 +169,6 @@ class Preprocessor extends CompilerBase $this->registerNativeGlobalObject($slot['name'], $slot['class'], $slot['node']); } } - - // The resolver closures are bound to the Translator. Release the - // short-lived discovery object before returning so an embedded, - // self-hosted compiler never keeps that object graph alive until - // php_embed_shutdown(). - unset($discovery); } public function getSortedFiles(array $list): array diff --git a/tests/compiler/native-class/global-and-static.phpt b/tests/compiler/native-class/global-and-static.phpt index e491eec6..0a3b04df 100644 --- a/tests/compiler/native-class/global-and-static.phpt +++ b/tests/compiler/native-class/global-and-static.phpt @@ -44,6 +44,27 @@ function globalIsNull(): bool return is_null($nativeGlobal); } +function initializeGlobalsArray(): void +{ + $GLOBALS['nativeGlobalsArray'] ??= new NativeCounter(); + $GLOBALS['nativeGlobalsArray']->value = 42; +} + +function readGlobalsArray(): int +{ + return $GLOBALS['nativeGlobalsArray']->value; +} + +function resetGlobalsArray(): void +{ + $GLOBALS['nativeGlobalsArray'] = null; +} + +function globalsArrayIsNull(): bool +{ + return is_null($GLOBALS['nativeGlobalsArray']); +} + function nextStatic(): int { static $counter = new NativeCounter(); @@ -60,6 +81,12 @@ function main(): void var_dump(globalIsNull()); replaceGlobalWithChild(); var_dump(readGlobal()); + initializeGlobalsArray(); + var_dump(readGlobalsArray()); + resetGlobalsArray(); + var_dump(globalsArrayIsNull()); + initializeGlobalsArray(); + var_dump(readGlobalsArray()); var_dump(nextStatic()); var_dump(nextStatic()); } @@ -69,5 +96,8 @@ int(40) int(41) bool(true) int(41) +int(42) +bool(true) +int(42) int(1) int(2) diff --git a/tests/compiler/native-class/value-selection.phpt b/tests/compiler/native-class/value-selection.phpt index b64aa340..5188941a 100644 --- a/tests/compiler/native-class/value-selection.phpt +++ b/tests/compiler/native-class/value-selection.phpt @@ -15,6 +15,45 @@ class NativeSelectedValue } } +#[Native] +class NativeSelectedBase +{ + public int $value; + + public function __construct(int $value) + { + $this->value = $value; + } +} + +#[Native] +class NativeSelectedLeft extends NativeSelectedBase +{ +} + +#[Native] +class NativeSelectedRight extends NativeSelectedBase +{ +} + +function selectSibling(bool $left): NativeSelectedBase +{ + return $left ? new NativeSelectedLeft(90) : new NativeSelectedRight(91); +} + +function matchSibling(bool $left): NativeSelectedBase +{ + return match ($left) { + true => new NativeSelectedLeft(92), + false => new NativeSelectedRight(93), + }; +} + +function coalesceSibling(?NativeSelectedLeft $left): NativeSelectedBase +{ + return $left ?? new NativeSelectedRight(94); +} + function selectWithMatch(int $kind): NativeSelectedValue { return match ($kind) { @@ -58,6 +97,11 @@ function main(): void $existing = new NativeSelectedValue(80); $existing ??= identitySelectedValue(makeSelectedValue()); var_dump($existing->value); + + var_dump(selectSibling(true)->value, selectSibling(false)->value); + var_dump(matchSibling(true)->value, matchSibling(false)->value); + var_dump(coalesceSibling(null)->value); + var_dump(coalesceSibling(new NativeSelectedLeft(95))->value); } ?> --EXPECT-- @@ -69,3 +113,9 @@ int(1) int(40) int(60) int(80) +int(90) +int(91) +int(92) +int(93) +int(94) +int(95)