Native Class 支持 [第九阶段]

master
韩天峰 1 week ago
parent c9666664aa
commit 2e6ddf98f1
  1. 131
      docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md
  2. 94
      docs/NATIVE_CLASS_OBJECT.md
  3. 17
      phpunit/code/native-class-global-forward/a.php
  4. 51
      phpunit/code/native-class-global-forward/b.php
  5. 16
      phpunit/src/NativeClass/NativeClassValidationTest.php
  6. 23
      run-tests.php
  7. 4
      src/CompilerBase.php
  8. 56
      src/NativeClass/NativeClassSupportTrait.php
  9. 87
      src/NativeClass/NativeGlobalDiscovery.php
  10. 147
      src/NativeClass/NativeGlobalTypeResolver.php
  11. 5
      src/Parser/ArrayExpressionTrait.php
  12. 46
      src/Parser/AssignOpTrait.php
  13. 5
      src/Parser/SelectionExpressionTrait.php
  14. 105
      src/Preprocessor.php
  15. 30
      tests/compiler/native-class/global-and-static.phpt
  16. 50
      tests/compiler/native-class/value-selection.phpt

@ -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 影响普通对象模型。

@ -1,8 +1,9 @@
# Native Class Object 设计与实现 # Native Class Object 设计与实现
> 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、 > 状态:核心方案已实现,正在进行最终整体验证。固定布局、Native Call、精确 tracing GC、
> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派和 > 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派、
> Interface 编译期契约已经落地;本文同时记录尚未开放的边界。 > Interface 编译期契约及项目级 global slot 预发现均已落地。逐项实现证据见
> [NATIVE_CLASS_IMPLEMENTATION_AUDIT.md](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。
## 1. 背景 ## 1. 背景
@ -46,7 +47,7 @@ Native Class Object 初版不追求以下能力:
## 4. 显式声明 ## 4. 显式声明
建议使用专用注解,暂定为 使用专用内置注解:
```php ```php
#[Native] #[Native]
@ -1292,49 +1293,43 @@ $json = json_encode($nativeObject->toArray());
| Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 | | Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 |
| late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 | | late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 |
## 17. 编译器目录与隔离要求 ## 17. 实际目录与隔离方式
Native Class 的实现应集中放置在独立目录 当前实现把对象模型的主体规则集中在以下位置
```text ```text
src/NativeClass/ src/NativeClass/
├── Analysis/ ├── NativeClassSupportTrait.php # 声明、布局、方法、边界和 codegen 策略
│ ├── NativeClassAnalyzer.php ├── NativeGlobalDiscovery.php # 项目级 Native global slot 预发现
│ ├── NativeEscapeAnalyzer.php └── NativeGlobalTypeResolver.php # 预发现器的只读符号查询边界
│ ├── NativeBoundaryValidator.php
│ └── NativeRootAnalyzer.php src/Transform/NativeClassAttributeLowering.php
├── CodeGen/ src/TypeSystem/NativeTypeCompatibilityTrait.php
│ ├── NativeClassGenerator.php
│ ├── NativeMethodGenerator.php phpx/include/phpx_native_gc.h
│ ├── NativePropertyGenerator.php phpx/src/core/native_gc.cc
│ ├── NativeTraceGenerator.php phpx/thirdparty/wren-gc/
│ └── NativeRootFrameGenerator.php
├── Model/
│ ├── NativeClassDefinition.php
│ ├── NativeFieldDefinition.php
│ └── NativeMethodDefinition.php
├── Transform/
│ ├── NativeAnnotationExpander.php
│ ├── NativeCloneLowering.php
│ └── NativePropertyHookLowering.php
├── Diagnostics/
│ └── NativeClassDiagnostic.php
└── NativeClassCompiler.php
``` ```
隔离原则: 普通 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 ```text
tests/compiler/native-class/ tests/compiler/native-class/
phpunit/src/NativeClass/NativeClassValidationTest.php
``` ```
普通 class 的测试与 Native Class 测试不得混用,以明确两套对象模型的语义边界。 普通 class 的测试与 Native Class 测试不得混用,以明确两套对象模型的语义边界。
@ -1376,21 +1371,24 @@ Native Class 的主要路径必须满足:
若某个 PHP 特性无法满足这些要求,应优先禁止该特性,而不是降低所有 Native Class 的性能。 若某个 PHP 特性无法满足这些要求,应优先禁止该特性,而不是降低所有 Native Class 的性能。
## 20. 建议的实施阶段 ## 20. 实施状态
实现按以下顺序推进 已落地的实施阶段
1. 固定 `#[Native] class` 语法、typed object 规则和诊断边界。 1. `#[Native] class` 语法、typed pointer 规则和编译期诊断边界。
2. 建立独立 AST model、side table 和 C++ struct 生成器。 2. C++ struct、固定字段、descriptor、trace 和 `php_*` 方法生成。
3. 建立 Native Heap、精确 tracing GC、root frame、循环回收和异常清理机制。 3. Wren 风格 Native Heap、精确 root frame、循环回收、异常恢复和 request shutdown。
4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。 4. 构造、析构、属性类型、PHPX 字段、普通方法及 typed pointer 参数/返回。
5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。 5. Trait AST 注入、Getter/Setter 和关键词方法直接调用。
6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。 6. 单继承、abstract、override virtual thunk、签名 variance 和 Interface 编译期契约。
7. 支持 Property Hook 的直接 getter/setter lowering;复合写入等动态语义编译期拒绝。 7. Property Hook 直接 getter/setter lowering,并拒绝所有间接与复合写入。
8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。 8. clone、动态子类 clone、循环类型、生命周期失败和对象复活处理。
9. 最后评估 `json_encode()`、栈分配与逃逸分析。 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. 已确定但仍需性能验证的参数 ## 21. 已确定但仍需性能验证的参数

@ -5,3 +5,20 @@ function readNativeForwardGlobal(): int
global $nativeForwardGlobal; global $nativeForwardGlobal;
return $nativeForwardGlobal->value; 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;
}

@ -20,3 +20,54 @@ class NativeForwardGlobalFactory
$nativeForwardGlobal = $local; $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();
}

@ -47,6 +47,22 @@ final class NativeClassValidationTest extends \BaseTest
'php::nativeDeref(nativeForwardGlobal, "NativeForwardGlobalValue").value', 'php::nativeDeref(nativeForwardGlobal, "NativeForwardGlobalValue").value',
$code, $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); self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code);
} }

@ -39,6 +39,8 @@ Options:
-j<workers>, -j <workers>, --job <workers> -j<workers>, -j <workers>, --job <workers>
Run up to <workers> simultaneous testing processes in parallel Run up to <workers> simultaneous testing processes in parallel
for quicker testing on systems with multiple logical processors. 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 <file> Read the testfiles to be executed from <file>. After the test -l <file> Read the testfiles to be executed from <file>. After the test
has finished all failed tests are written to the same <file>. has finished all failed tests are written to the same <file>.
@ -4439,6 +4441,7 @@ function compile_wasm_php_file(string $file, string $profile, string $compilerAr
if ($compilerArgs !== '') { if ($compilerArgs !== '') {
array_push($command, ...parse_wasm_test_args($compilerArgs)); array_push($command, ...parse_wasm_test_args($compilerArgs));
} }
array_push($command, ...get_aot_compiler_job_args());
$log = $root . DIRECTORY_SEPARATOR . 'compile.log'; $log = $root . DIRECTORY_SEPARATOR . 'compile.log';
$process = proc_open( $process = proc_open(
@ -4576,6 +4579,9 @@ function compile_php_file(string $file, string $compiler_args = ''): string
if ($compiler_args !== '') { if ($compiler_args !== '') {
$cmd .= ' ' . $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); exec($cmd . $compiler_output_args . ' 2>&1', $output, $exitCode);
clearstatcache(true, $binary_file); clearstatcache(true, $binary_file);
@ -4586,6 +4592,23 @@ function compile_php_file(string $file, string $compiler_args = ''): string
return $binary_file; 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<string>
*/
function get_aot_compiler_job_args(): array
{
global $workers;
return $workers !== null && $workers > 1 ? ['--job', '1'] : [];
}
function create_aot_parallel_root(): string function create_aot_parallel_root(): string
{ {
$suffix = getmypid() . '-' . bin2hex(random_bytes(6)); $suffix = getmypid() . '-' . bin2hex(random_bytes(6));

@ -1974,6 +1974,10 @@ class CompilerBase implements PropertyAccessContext
return $this->getObjectType($object); 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)) { if ($expr instanceof Expr\PropertyFetch && $this->isIdExpr($expr->name)) {
$receiverClass = $this->detectClassOfExpr($expr->var); $receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) { if ($this->isNativeObjectClass($receiverClass)) {

@ -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(); $name = $type->getName();
$lower = strtolower($name); $lower = strtolower($name);
if ($lower === 'self') { 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 protected function getNativeObjectCppName(string|ClassDef $class): string
{ {
if ($class instanceof ClassDef) { if ($class instanceof ClassDef) {
@ -523,18 +545,36 @@ trait NativeClassSupportTrait
$common = $class; $common = $class;
continue; continue;
} }
if ($this->isObjectClassStaticallyAssignableTo($class, $common)) { $common = $this->getCommonNativeObjectClass($common, $class);
continue; if ($common === '') {
} return '';
if ($this->isObjectClassStaticallyAssignableTo($common, $class)) {
$common = $class;
continue;
} }
return '';
} }
return $common; 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( protected function assertSupportedNativeObjectTypeNode(
?NodeAbstract $type, ?NodeAbstract $type,
int $declarationKind, int $declarationKind,

@ -1,6 +1,6 @@
<?php <?php
/** /**
* This file is part of TypePHP. * This file is part of Swoole-Compiler(AOT).
* *
* @link https://www.swoole.com/ * @link https://www.swoole.com/
* @contact service@swoole.com * @contact service@swoole.com
@ -22,54 +22,21 @@ use PhpParser\NodeAbstract;
*/ */
final class NativeGlobalDiscovery final class NativeGlobalDiscovery
{ {
/** @var \Closure(string): ?string */ private NativeGlobalTypeResolver $resolver;
private \Closure $canonicalClass;
/** @var \Closure(string): ?string */
private \Closure $nativeClass;
/** @var \Closure(list<string>): ?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;
/** @var array<string, string> */
private array $functionReturns;
private string $scopeClass = ''; private string $scopeClass = '';
/** /**
* @param \Closure(string): ?string $canonicalClass * @param array<string, string> $functionReturns
* @param \Closure(string): ?string $nativeClass
* @param \Closure(list<string>): ?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
*/ */
public function __construct( public function __construct(
\Closure $canonicalClass, NativeGlobalTypeResolver $resolver,
\Closure $nativeClass, array $functionReturns,
\Closure $functionReturn,
\Closure $methodReturn,
\Closure $propertyClass,
\Closure $commonClass,
\Closure $parentClass,
) { ) {
$this->canonicalClass = $canonicalClass; $this->resolver = $resolver;
$this->nativeClass = $nativeClass; $this->functionReturns = $functionReturns;
$this->functionReturn = $functionReturn;
$this->methodReturn = $methodReturn;
$this->propertyClass = $propertyClass;
$this->commonClass = $commonClass;
$this->parentClass = $parentClass;
} }
/** /**
@ -129,7 +96,7 @@ final class NativeGlobalDiscovery
$locals = []; $locals = [];
$previousScope = $this->scopeClass; $previousScope = $this->scopeClass;
$thisClass = ($this->canonicalClass)($class); $thisClass = $this->resolver->canonicalClass($class);
$this->scopeClass = $thisClass ?? ''; $this->scopeClass = $thisClass ?? '';
if ($thisClass !== null) { if ($thisClass !== null) {
$locals['this'] = $thisClass; $locals['this'] = $thisClass;
@ -226,11 +193,11 @@ final class NativeGlobalDiscovery
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) {
return; 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); $this->analyzeNodes($node->expr, $globals, $locals, $result);
$class = $this->inferClass($node->expr, $locals); $class = $this->inferClass($node->expr, $locals);
$globalName = $this->assignmentGlobalName($node->var, $globals); $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) { if ($globalName !== null && $nativeClass !== null) {
$result[] = ['name' => $globalName, 'class' => $nativeClass, 'node' => $node]; $result[] = ['name' => $globalName, 'class' => $nativeClass, 'node' => $node];
} elseif ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)) { } elseif ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)) {
@ -251,7 +218,7 @@ final class NativeGlobalDiscovery
/** @param array<string, string> $locals */ /** @param array<string, string> $locals */
private function inferClass(NodeAbstract $expression, array $locals): ?string 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); return $this->inferClass($expression->expr, $locals);
} }
if ($expression instanceof Node\Expr\New_ && $expression->class instanceof Node\Name) { if ($expression instanceof Node\Expr\New_ && $expression->class instanceof Node\Name) {
@ -266,26 +233,38 @@ final class NativeGlobalDiscovery
return $this->inferClass($expression->expr, $locals); return $this->inferClass($expression->expr, $locals);
} }
if ($expression instanceof Node\Expr\FuncCall && $expression->name instanceof Node\Name) { 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 if ($expression instanceof Node\Expr\StaticCall
&& $expression->class instanceof Node\Name && $expression->class instanceof Node\Name
&& $expression->name instanceof Node\Identifier && $expression->name instanceof Node\Identifier
) { ) {
$class = $this->resolvedClass($expression->class); $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 if ($expression instanceof Node\Expr\MethodCall
&& $expression->name instanceof Node\Identifier && $expression->name instanceof Node\Identifier
) { ) {
$class = $this->inferClass($expression->var, $locals); $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 if ($expression instanceof Node\Expr\PropertyFetch
&& $expression->name instanceof Node\Identifier && $expression->name instanceof Node\Identifier
) { ) {
$class = $this->inferClass($expression->var, $locals); $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 instanceof Node\Expr\Ternary) {
$if = $expression->if === null $if = $expression->if === null
@ -317,7 +296,7 @@ final class NativeGlobalDiscovery
if ($right === null) { if ($right === null) {
return $left; return $left;
} }
return ($this->commonClass)($left, $right); return $this->resolver->commonClass($left, $right);
} }
private function classFromType(?NodeAbstract $type): ?string private function classFromType(?NodeAbstract $type): ?string
@ -373,9 +352,11 @@ final class NativeGlobalDiscovery
return $this->scopeClass !== '' ? $this->scopeClass : null; return $this->scopeClass !== '' ? $this->scopeClass : null;
} }
if ($keyword === 'parent') { 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<string> */ /** @return list<string> */

@ -0,0 +1,147 @@
<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\NativeClass;
use TypePhp\Entity\ClassDef;
/**
* Immutable class metadata used by the Native global pre-pass.
*
* The analyzer needs no reference back to the compiler or its mutable
* per-function context. This keeps the project-level pass deterministic and
* prevents a short-lived analysis object from retaining the Translator.
*/
final class NativeGlobalTypeResolver
{
/** @var array<string, string> */
private array $classes = [];
/** @var array<string, true> */
private array $nativeClasses = [];
/** @var array<string, string> */
private array $parents = [];
/** @var array<string, array<string, ?string>> */
private array $methodReturns = [];
/** @var array<string, array<string, ?string>> */
private array $propertyClasses = [];
/** @param array<string, ClassDef> $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;
}
}

@ -96,7 +96,10 @@ trait ArrayExpressionTrait
$this->addGlobalVar($name, Type::VAR); $this->addGlobalVar($name, Type::VAR);
} }
if (!$this->hasScopeGlobalVar($name)) { 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; return $name;
} }

@ -32,9 +32,25 @@ trait AssignOpTrait
} }
if ($this->isVarExpr($left->var) && $left->var->name === 'GLOBALS') { if ($this->isVarExpr($left->var) && $left->var->name === 'GLOBALS') {
$target = $this->parseGlobalsArrayDimFetch($left); $target = $this->parseGlobalsArrayDimFetch($left);
$value = $this->parseExprAsValue($right);
$tmp = $this->genTmpVarName(); $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 . ')'; return '((' . $tmp . ' = ' . $value . ', ' . $target . ' = ' . $tmp . '), ' . $tmp . ')';
} }
$array = $this->parseWritableIdentifier($left->var); $array = $this->parseWritableIdentifier($left->var);
@ -259,6 +275,15 @@ trait AssignOpTrait
if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) {
$this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right); $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)) { } elseif ($left instanceof Expr\PropertyFetch && $this->isIdExpr($left->name)) {
$receiverClass = $this->detectClassOfExpr($left->var); $receiverClass = $this->detectClassOfExpr($left->var);
if ($this->isNativeObjectClass($receiverClass)) { if ($this->isNativeObjectClass($receiverClass)) {
@ -1164,6 +1189,19 @@ trait AssignOpTrait
// declared as Variant because boxing the raw pointer would coerce it // declared as Variant because boxing the raw pointer would coerce it
// to bool. Other values retain the normal nullable Variant behavior. // to bool. Other values retain the normal nullable Variant behavior.
$var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; $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 ($var !== null && !$this->hasVar($var)) {
if ($nativeRight) { if ($nativeRight) {
$this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); $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); $var ??= $this->parseWritableIdentifier($expr->var);
$propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var);

@ -45,11 +45,16 @@ trait SelectionExpressionTrait
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount); $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount);
if ($nativeSelection) { if ($nativeSelection) {
$pointerType = $this->getNativeObjectPointerType($nativeClass);
if ($this->isNull($expr->if)) { if ($this->isNull($expr->if)) {
$if = 'nullptr'; $if = 'nullptr';
} else {
$if = 'static_cast<' . $pointerType . '>(' . $if . ')';
} }
if ($this->isNull($expr->else)) { if ($this->isNull($expr->else)) {
$else = 'nullptr'; $else = 'nullptr';
} else {
$else = 'static_cast<' . $pointerType . '>(' . $else . ')';
} }
} }

@ -31,6 +31,7 @@ use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\RuntimeAttributeFactoryLowering; use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use TypePhp\Transform\Visitor; use TypePhp\Transform\Visitor;
use TypePhp\NativeClass\NativeGlobalDiscovery; use TypePhp\NativeClass\NativeGlobalDiscovery;
use TypePhp\NativeClass\NativeGlobalTypeResolver;
use PhpParser\Modifiers; use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator; use PhpParser\ConstExprEvaluator;
use PhpParser\Node; use PhpParser\Node;
@ -126,6 +127,22 @@ class Preprocessor extends CompilerBase
return; 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 = []; $functionReturns = [];
foreach ($this->symbols->functions() as $function) { foreach ($this->symbols->functions() as $function) {
if (!$function->method && $this->hasClass($function->returnClass)) { if (!$function->method && $this->hasClass($function->returnClass)) {
@ -134,86 +151,10 @@ class Preprocessor extends CompilerBase
} }
} }
$discovery = new NativeGlobalDiscovery( $resolver = new NativeGlobalTypeResolver($this->symbols->classes());
function (string $class): ?string { $discovery = new NativeGlobalDiscovery($resolver, $functionReturns);
$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;
},
);
foreach ($files as $file) { foreach ($candidateSources as $source) {
if (!$this->isPhpFileForNativeDiscovery($file)) {
continue;
}
$source = file_get_contents($file);
if (!is_string($source)
|| (!str_contains($source, 'global') && !str_contains($source, '$GLOBALS'))
) {
continue;
}
try { try {
$ast = $this->parser->parse($source); $ast = $this->parser->parse($source);
} catch (\PhpParser\Error) { } catch (\PhpParser\Error) {
@ -228,12 +169,6 @@ class Preprocessor extends CompilerBase
$this->registerNativeGlobalObject($slot['name'], $slot['class'], $slot['node']); $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 public function getSortedFiles(array $list): array

@ -44,6 +44,27 @@ function globalIsNull(): bool
return is_null($nativeGlobal); 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 function nextStatic(): int
{ {
static $counter = new NativeCounter(); static $counter = new NativeCounter();
@ -60,6 +81,12 @@ function main(): void
var_dump(globalIsNull()); var_dump(globalIsNull());
replaceGlobalWithChild(); replaceGlobalWithChild();
var_dump(readGlobal()); var_dump(readGlobal());
initializeGlobalsArray();
var_dump(readGlobalsArray());
resetGlobalsArray();
var_dump(globalsArrayIsNull());
initializeGlobalsArray();
var_dump(readGlobalsArray());
var_dump(nextStatic()); var_dump(nextStatic());
var_dump(nextStatic()); var_dump(nextStatic());
} }
@ -69,5 +96,8 @@ int(40)
int(41) int(41)
bool(true) bool(true)
int(41) int(41)
int(42)
bool(true)
int(42)
int(1) int(1)
int(2) int(2)

@ -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 function selectWithMatch(int $kind): NativeSelectedValue
{ {
return match ($kind) { return match ($kind) {
@ -58,6 +97,11 @@ function main(): void
$existing = new NativeSelectedValue(80); $existing = new NativeSelectedValue(80);
$existing ??= identitySelectedValue(makeSelectedValue()); $existing ??= identitySelectedValue(makeSelectedValue());
var_dump($existing->value); 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-- --EXPECT--
@ -69,3 +113,9 @@ int(1)
int(40) int(40)
int(60) int(60)
int(80) int(80)
int(90)
int(91)
int(92)
int(93)
int(94)
int(95)

Loading…
Cancel
Save