feat(compiler): implement typed reference system for native C++ integration

- Add five new reference types (INT_REF, STR_REF, FLOAT_REF, BOOL_REF, ARRAY_REF)
- Create typed reference ABI using C++ references for strict type safety
- Implement RefWrap<T> bridge for dynamic PHP reference compatibility
- Add getRawVarType() and update getVarType() to handle reference unwrapping
- Enforce strict typing rules preventing fixed storage corruption via references
- Support typed property and array element reference handling
- Implement exception-safe reference binding with proper
master
韩天峰 1 month ago
parent 2166e1a1a1
commit 50a8ce1570
  1. 15
      CHANGELOG.md
  2. 16
      docs/en/INCOMPATIBLE_PHP_FEATURES.md
  3. 11
      docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md
  4. 438
      docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md
  5. 16
      src/CompilerBase.php

@ -27,9 +27,12 @@ to files that genuinely depend on Zend integer widening semantics.
Fixed-storage locals can no longer be converted to PHP references. This rule Fixed-storage locals can no longer be converted to PHP references. This rule
applies to native scalars, strings, arrays, objects, streams, high-precision applies to native scalars, strings, arrays, objects, streams, high-precision
values, and `std` containers. Zend references are untyped, so allowing one to values, and `std` containers. A reference made from an ordinary local has no
alias fixed C++ storage could corrupt the variable's type. Use `std::any()` Zend type source, so exposing fixed C++ storage through it could corrupt the
before reference operations and explicitly convert the result back afterward. variable's type. Typed object/static properties remain reference-capable: Zend
records their property metadata as a type source. PHP array elements remain
dynamic reference-capable slots. Use `std::any()` when a local needs reference
semantics and explicitly convert the result back afterward.
TypePHP is always strict. Project sources no longer need TypePHP is always strict. Project sources no longer need
`declare(strict_types=1)`; the directive remains accepted as a redundant PHP `declare(strict_types=1)`; the directive remains accepted as a redundant PHP
@ -98,8 +101,10 @@ should review the change log and run their full test suite before upgrading.
`use native_types`,并且只为真正依赖 Zend 整数扩展语义的文件添加新兼容模式。 `use native_types`,并且只为真正依赖 Zend 整数扩展语义的文件添加新兼容模式。
固定存储的局部变量不再允许转换为 PHP 引用,包括原生标量、字符串、数组、对象、 固定存储的局部变量不再允许转换为 PHP 引用,包括原生标量、字符串、数组、对象、
stream、高精度值和 `std` 容器。Zend 引用没有类型约束,若允许其指向固定 C++ 存储, stream、高精度值和 `std` 容器。普通局部变量产生的引用没有 Zend type source,若允许
可能破坏变量类型。需要引用操作时先使用 `std::any()`,操作完成后再显式转换回目标类型。 其指向固定 C++ 存储,可能破坏变量类型。Typed Property 仍然可以取引用:Zend 会把属性
元数据记录为 type source;PHP 数组元素也仍是可取引用的动态槽位。局部变量需要引用
语义时先使用 `std::any()`,操作完成后再显式转换回目标类型。
TypePHP 始终使用严格类型,项目源码不再需要 `declare(strict_types=1)`。该声明仍作为 TypePHP 始终使用严格类型,项目源码不再需要 `declare(strict_types=1)`。该声明仍作为
冗余的 PHP 兼容语法被接受,而 `strict_types=0` 会被拒绝。 冗余的 PHP 兼容语法被接受,而 `strict_types=0` 会被拒绝。

@ -105,13 +105,15 @@ incompatible with or more restrictive than standard PHP.
properties. properties.
- A local with fixed storage (`Int`, `Float`, `Bool`, `Str`, `Array`, `Object`, - A local with fixed storage (`Int`, `Float`, `Bool`, `Str`, `Array`, `Object`,
`Stream`, high-precision values, or a `std` container) cannot be made into a `Stream`, high-precision values, or a `std` container) cannot be made into a
PHP reference. Zend references are untyped and could replace such storage PHP reference. An ordinary local reference has no Zend type source and could
with an incompatible value. Objects, streams, typed objects, and `std` replace such storage with an incompatible value. Objects, streams, typed
containers already use handle/reference-like value semantics, so adding a objects, and `std` containers already use handle/reference-like value
PHP reference to the local variable is unnecessary as well. Initialize the semantics, so adding a PHP reference to the local variable is unnecessary as
value with `std::any()` when PHP reference semantics are required, then well. Typed object/static properties remain reference-capable because Zend
convert it back explicitly with a keyword such as `toArray()` or attaches their property metadata to the reference; PHP array elements remain
`toString()`. dynamic reference-capable slots. Initialize a local with `std::any()` when
PHP reference semantics are required, then convert it back explicitly with a
keyword such as `toArray()` or `toString()`.
- A call that uses argument unpacking followed by named arguments falls back to - A call that uses argument unpacking followed by named arguments falls back to
dynamic dispatch and cannot use the native call path. dynamic dispatch and cannot use the native call path.

@ -49,11 +49,12 @@
- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。 - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。
- `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。 - `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。
- 采用固定存储的局部变量(`Int`、`Float`、`Bool`、`Str`、`Array`、`Object`、 - 采用固定存储的局部变量(`Int`、`Float`、`Bool`、`Str`、`Array`、`Object`、
`Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。Zend 引用没有类型约束, `Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。普通局部引用没有 Zend
可能把固定存储替换成不兼容的值。对象、stream、typed object 与 `std` 容器本身 type source,可能把固定存储替换成不兼容的值。对象、stream、typed object 与
已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。确需 PHP 引用 `std` 容器本身已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。
语义时,应使用 `std::any()` 初始化,完成引用操作后再通过 `toArray()`、 Typed Property 仍可取引用,Zend 会将属性元数据附加到引用上;PHP 数组元素仍是
`toString()` 等关键词显式转换回来。 可取引用的动态槽位。局部变量确需 PHP 引用语义时,应使用 `std::any()` 初始化,
完成引用操作后再通过 `toArray()`、`toString()` 等关键词显式转换回来。
- 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。
## 对象模型 ## 对象模型

@ -0,0 +1,438 @@
# 强类型原生引用设计
本文描述 TypePHP 0.8 的强类型引用模型。这里的“强类型引用”是 C++ 引用,
不是 `php::Ref` 的另一种名字。
## 目标
TypePHP 默认把 `int`、`string`、`float`、`bool`、`array` 保存为固定 C++
类型。引用不得迫使这些变量退化为 `php::Var`,也不得允许动态代码静默改变
它们的类型。
```php
$value = 100;
$alias =& $value;
```
上述变量的逻辑类型分别为 `int` 和 `int&`。在可证明安全的原生路径中,生成
等价的 C++ 引用,而不是创建 Zend reference:
```cpp
php::Int value = 100;
php::Int &alias = value;
```
## 类型系统
新增五种引用类型:
| TypePHP 类型 | 引用目标 | C++ ABI |
| --- | --- | --- |
| `INT_REF` | `int` | `php::Int &` |
| `STR_REF` | `string` | `php::Str &` |
| `FLOAT_REF` | `float` | `php::Float &` |
| `BOOL_REF` | `bool` | `php::Bool &` |
| `ARRAY_REF` | `array` | `php::Array &` |
`REF` 继续专指动态 PHP 引用,C++ 存储为 `php::Ref`。不能把 typed-ref 与
`REF` 合并,否则编译器会丢失引用目标类型,赋值、运算和静态调用都将退化为
动态 `zval` 操作。
以下帮助函数必须成为唯一的类型转换入口:
- `isTypedRefType($type)`:是否为五种 typed-ref;
- `getReferenceType($type)`:值类型转为 typed-ref;
- `getReferencedType($type)`:typed-ref 转为值类型;
- `getReferenceCppType($type)`:生成 `T &` ABI;
- `isAnyRefType($type)`:typed-ref 或动态 `REF`。
SSA 只能保留或缩窄引用目标类型,不得把 typed-ref 提升成 `VAR/REF`。
类型系统必须区分三个概念:
- **值类型**:表达式参与运算、普通赋值和按值传参时看到的 `INT/STR/...`;
- **绑定类型**:变量槽位是普通值、typed-ref,还是动态 `REF`;
- **引用根**:多个 typed-ref 最终指向的同一个固定存储变量。
例如 `$d =& $b` 中,`$b/$d` 的值类型都是 `INT`,绑定类型都是 `INT_REF`,
引用根都是 `$a`。绝大多数表达式只查询值类型;引用赋值、ABI、生命周期和移动优化
必须查询原始绑定类型,不能用解除引用后的类型代替。
建议在函数上下文中保存:
```text
localVars[$name] = INT | INT_REF | REF | ...
typedRefRoots[$name] = $rootName
typedRefBindings[$name] = $targetName
```
`getVarType()` 返回表达式值类型,`getRawVarType()` 返回绑定类型。
## 参数 ABI
带有五种明确类型的引用参数使用 C++ 引用:
```php
function increment(int &$value): void
{
$value++;
}
```
```cpp
void php_increment(php::Int &value);
```
无类型、`mixed`、`any`、union 或运行时才能确定的引用参数继续使用
`php::Ref`。对象、resource、stream、Native Object、typed object、Box 和
`std` container 不支持 typed-ref;这些值本来就使用句柄、指针或共享对象语义,
再对变量槽位取引用没有明确收益,反而允许调用方替换其静态类型。
## 属性和数组元素
普通对象属性及静态属性必须通过 Zend 属性 API 取得 `php::Ref`。Typed
Property 创建 reference 时,Zend 会把 `zend_property_info` 附加为 type
source,后续写入仍受属性声明类型约束。不能把属性读成临时值后再创建引用,
否则会丢失该 type source。
PHP 数组元素是动态 `zval` 槽位,引用统一使用 `php::Ref`。数组元素也可能与
Typed Property 共享同一个 `zend_reference`,因此桥接层必须保留原始 reference,
不得复制或解除引用。
当 typed-ref 参数接收属性或数组元素时,由 `php::RefWrap<T>` 在栈上把
`php::Ref` 转成 `T&`:
```cpp
php::Ref property = /* attrRef() */;
php::RefWrap<php::Str> bridge(property);
php_update_name(bridge.typed());
```
构造时必须验证当前值类型。`Int/Float` 可以直接引用 reference 内部 `zval`
payload;`Str/Array` 使用安全的 indirect wrapper,使原生函数的每次写入仍作用于
原始槽位。Zend 的 `bool` 只编码为 `IS_TRUE/IS_FALSE` 类型标签,并不存在可取地址
的 `bool` payload,因此 `Bool&` 必须使用栈上代理,并在本次调用结束时写回。
typed-ref 函数只能写入 `T`,因此不会破坏属性类型。
## typed-ref 转换为动态 PHP 引用
动态函数、动态方法和 Closure 的引用签名在编译期不可确定。调用者显式使用
`std::ref()` 时,`php::RefWrap<T>` 为 typed local 创建临时 `php::Ref`:
```cpp
php::Int value = 1;
php::RefWrap<php::Int> bridge(value);
php::call(callback, php::VarList{&bridge.ref()});
bridge.commit();
```
`commit()` 必须检查动态调用后的 zval 仍与 `T` 一致,然后写回 typed local;
类型不一致时抛出统一的 TypeError。不能依赖一个可能抛异常的析构函数完成检查。
桥接生命周期必须是“一次调用表达式”,不能是整个 PHP 语句。嵌套调用中,内层
调用返回后,必须在求值外层调用的下一个实参之前完成 `commit()`:
```php
outer(dynamicCall(std::ref($value)), nextArgument());
```
这里 `dynamicCall()` 的写回必须先于 `nextArgument()`。因此不能用语句级
`beforeStmtLines/afterStmtLines` 实现,而应把 wrapper、调用、成功写回和异常写回
统一放入一个立即执行的表达式 helper/lambda 中。多个 typed-ref 实参若指向同一变量,
必须复用同一个 bridge,不能创建内容互相覆盖的临时 reference。
调用抛出异常时,桥接层仍要按 PHP 的可观察语义处理已经发生的引用修改。生成代码
必须在重新抛出原异常前执行非破坏性的写回;若写回又发现类型错误,以类型错误替换
或链接到原异常的具体策略应由一个 PHPX helper 统一实现,不能在每个调用点复制
Zend 异常处理代码。
### `RefWrap` 的两种方向
`RefWrap<T>` 有两种互斥状态:
1. `T& -> php::Ref`:为动态调用创建临时 Zend reference,调用结束后验证并写回;
2. `php::Ref -> T&`:为静态 typed-ref 参数提供原生引用视图。
建议内部状态明确表示为 `NativeToZend`、`ConstrainedZendToNative`、
`DynamicZendToNative`,禁止用若干松散布尔值组合隐含方向。
| `T` | `php::Ref -> T&` 实现 | 是否需要写回代理 |
| --- | --- | --- |
| `Int` | 直接指向 `Z_LVAL_P(refval)` | 否 |
| `Float` | 直接指向 `Z_DVAL_P(refval)` | 否 |
| `Bool` | 栈上 `Bool` 代理 | 是 |
| `Str` | 指向 refval 的 indirect `Str` | 否 |
| `Array` | 指向 refval 的 indirect `Array` | 否 |
`Bool` 不能直接映射的原因来自 Zend ABI:布尔值由 zval 的 `IS_TRUE/IS_FALSE`
类型标签表达,没有独立的 `bool` payload。
“存在可寻址 payload”不代表任何 reference 都可以无条件直连。普通 PHP reference
可能还有其他动态别名;typed 函数内部若发生重入调用,动态代码可能把同一个 refval
改成其他类型。随后继续通过 `Int&/Float&` 访问已经不再活动的 union member 是不安全的。
因此 `php::Ref -> T&` 分成两种安全级别:
- reference 带有与 `T` 一致的 Zend typed-property type source:Zend 会阻止重入代码
改变类型,`Int/Float/Str/Array` 可以直接映射;
- 普通无 type source 的 reference:必须使用隔离代理。调用结束时比较原始 reference
与入口快照,检测重入别名修改;出现双方同时修改的冲突时抛出明确错误,不能静默覆盖。
`Bool` 在两种情况下都使用代理。直接映射前必须检查 reference 的全部 type sources,
不能仅凭当前 `Z_TYPE` 推断其以后不会改变。
### 调用表达式边界
C++17 对普通函数参数只保证参数之间不会交错求值,不保证从左到右:
```cpp
outer(first(), second()); // first/second 谁先执行未指定
```
而 braced initializer list 保证从左到右。TypePHP 固定参数容器已经使用
`php::VarList{...}`,typed-ref bridge 必须继续保留这一性质。实测 GCC 会先执行上述
示例的第二个参数,不能依赖某个编译器当前的选择。
临时对象通常在包含它的 full-expression 结束时析构。若把 `RefWrap` 临时对象直接
放入外层调用参数,它可能直到外层调用返回后才析构。正确结构是让每个需要桥接的
内层调用拥有独立的立即执行作用域:
```cpp
auto result = [&]() {
php::RefWrap<php::Int> bridge(value);
try {
auto retval = dynamic_call(bridge.ref());
bridge.commit();
return retval;
} catch (zend_object *) {
bridge.commitAfterException();
throw;
}
}();
```
这里 `commit()` 和 `bridge` 析构都发生在 lambda 产生结果之前。外层
`VarList{inner(), next()}` 才能保证 `inner()` 写回先于 `next()`。
### 异常路径
动态调用在修改引用后抛出异常时,PHP 会保留已经发生的修改。桥接也必须在重新抛出
原异常之前写回类型兼容的值:
```php
function mutateThenThrow(int &$value): never
{
$value = 99;
throw new RuntimeException();
}
```
若临时 reference 被改成不兼容类型,则不能写入固定存储。无异常返回时抛出 TypeError;
已有异常正在传播时,bridge 保留原异常并丢弃不兼容写回,避免在异常展开期间再次抛出
C++ 异常。这个规则必须集中在 helper 中,析构函数始终保持 `noexcept`。
### 别名与逃逸
同一次动态调用中,如果两个实参最终拥有相同引用根,必须复用一个 `RefWrap`:
```php
$b =& $a;
$d =& $b;
$callback(std::ref($b), std::ref($d));
```
否则两个临时 reference 会各自保存一份值,提交顺序将覆盖修改,也破坏 PHP 的别名
身份。编译器应按 `typedRefRoots` 对 bridge 去重。
动态 PHP 代码还可能把引用保存到全局、对象属性或 Closure 中,使其超过本次调用。
栈上 native storage 无法安全支持这种逃逸。`RefWrap` 应在调用返回时检测临时
`zend_reference` 是否出现额外持有者;检测到逃逸时抛出明确错误。该限制保证不会把
已经失效的 C++ 地址暴露给 ZendVM。需要长期 PHP 引用身份的变量必须从一开始使用
`std::any()`/`php::Ref`。
## 局部引用约束
固定局部变量之间的一次性引用绑定可生成 C++ 引用:
```php
$source = 1;
$alias =& $source;
```
引用传播遵循 PHP 的值/引用上下文区别:
```php
$a = 100;
$b =& $a; // $b: INT_REF,引用 $a
$c = $b; // $c: INT,按值复制并解除引用
$d =& $b; // $d: INT_REF,与 $b 一样引用 $a
```
因此,对 typed-ref 再取引用仍得到同一目标的 typed-ref;typed-ref 出现在普通赋值
右侧或普通值参数位置时必须物化为值。移动/last-use 优化不得消费 typed-ref 所指向的
原变量,尤其不能对 `Str&`、`Array&` 生成 `takeValue()` 或 `std::move()`。
### typed-ref 普通赋值
普通赋值修改引用目标,不会重新绑定引用:
```php
$a = 100;
$intRef =& $a;
$intRef = 101; // 修改 $a
$intRef = '100'; // 编译期错误
```
检查规则如下:
- RHS 类型已知且不满足引用目标类型:TypePHP 编译期 fatal error;
- RHS 类型已知且兼容:按目标值类型生成赋值;
- RHS 为 `VAR/REF`,无法静态确定:只求值一次,执行运行时严格类型检查后赋值;
- 禁止让不兼容表达式穿透到 C++ 编译阶段形成难以理解的运算符/模板错误。
`$c = $intRef` 属于值上下文,`$c` 推导为 `INT`;`$d =& $intRef` 属于引用上下文,
`$d` 推导为 `INT_REF` 并继承相同引用根。
由于 C++ 引用不能重新绑定,初版作如下限制:
- typed-ref 只允许在两个函数局部变量之间建立;LHS 必须是尚未声明的新变量,RHS
必须是已经存在的固定值 local 或 typed-ref;
- typed-ref 必须在编译器可证明的单一绑定点创建;
- 禁止在分支、循环中首次绑定 typed-ref;
- 禁止把已有 typed-ref 重新绑定到另一个变量;
- 禁止 `unset()` typed-ref;
- 当一个 fixed local 已成为其他 typed-ref 的引用根时,也禁止 `unset()` 或把该根
重新绑定为另一个引用;
- `$array[] =& $typedLocal`、`$object->prop =& $typedLocal`、`global/static` 等会让引用
超过 native local 生命周期的写法一律禁止;
- `$alias =& $object->typedProperty`、`$alias =& $array[$key]` 继续产生动态 `REF`,
不产生长期持有 `RefWrap` 的 C++ 引用;
- 禁止返回指向函数局部存储的 typed-ref;
- 禁止 Closure 按引用捕获 typed local,因为 Closure 可能超过栈帧生命周期;
- 参数 typed-ref 可以继续传给静态可解析的 typed-ref 参数。
Generator、可挂起 Fiber 回调以及任何可能超过调用者栈帧的执行体,不得持有指向调用者
native local 的 `T&`。初版应禁止这些函数使用 typed-ref 参数,而不是尝试推断实际
挂起路径。
返回引用暂不使用 `T&` ABI。对象属性、静态属性和数组元素的返回引用需要保留 Zend
reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 `function &name()`
继续返回 `php::Ref`,并禁止返回 typed local/typed-ref 参数形成的临时 bridge。
无法证明稳定绑定时,用户必须显式使用 `std::any()` 选择动态 `php::Ref` 语义。
## 不变量
实现与优化器必须共同保持以下不变量:
1. **固定类型不变**:typed-ref 的目标在整个生命周期内始终是同一个 C++ 类型;
2. **绑定不变**:一个 C++ reference 构造后永不重新绑定;
3. **根唯一**:引用链被压缩到唯一 root,同一次调用中一个 root 只创建一个 bridge;
4. **只求值一次**:属性 receiver、数组 key、动态 callable 和每个参数表达式都不得重复求值;
5. **PHP 求值顺序**:参数及嵌套调用按照 PHP 的从左到右可观察顺序完成;
6. **调用级提交**:bridge 在本次调用完成后、下一个表达式开始前提交;
7. **不逃逸**:指向 native stack storage 的 Zend reference 不得在调用结束后存活;
8. **析构不抛异常**:所有失败通过显式 `commit()`/调用 helper 报告;
9. **值上下文解除引用**:普通赋值、按值参数、返回值和数组插入必须取得值快照;
10. **优化不得消费 root**:last-use、move、multi-return 和参数转发不得移动 typed-ref 的目标。
任何无法证明这些不变量的路径都应退回动态 `REF/VAR`(仅在源程序已经选择动态存储时),
或给出 TypePHP fatal error;不得生成“可能正确”的 C++。
## 语法到存储的决策表
| PHP 写法 | 结果 |
| --- | --- |
| 新 local `$b =& $a`,`$a` 是五种 fixed local | 对应的 `*_REF`,C++ `T&` |
| 新 local `$d =& $b`,`$b` 是 typed-ref | 相同 `*_REF`,引用根继承自 `$b` |
| `$c = $b`,`$b` 是 typed-ref | 对应值类型,按值复制 |
| `$b = $value`,`$b` 是 typed-ref | 检查后写入 root,不重新绑定 |
| `$b =& $other`,`$b` 已存在 | fatal error |
| `$b =& $property` / `$b =& $array[$key]` | 动态 `php::Ref` |
| `$property =& $typedLocal` / `$array[] =& $typedLocal` | fatal error,禁止 native 引用逃逸 |
| 精确 `int/string/float/bool/array &$arg` | native ABI `T&` |
| nullable/union/mixed/untyped/defaulted/variadic `&$arg` | Zend ABI `php::Ref` |
| object/resource/stream/container/typed-object `&$arg` | fatal error |
| 动态调用 `std::ref($typedLocal)` | 调用级 `RefWrap<T>` |
| 动态调用 `std::ref($property/arrayItem)` | 直接传原始 Zend reference |
## 调用分类
| 实参 | 静态 typed-ref 调用 | 动态调用/Closure |
| --- | --- | --- |
| typed local | 直接传 `T&` | `RefWrap<T>`,要求显式 `std::ref()` |
| typed-ref local/parameter | 直接传 `T&` | `RefWrap<T>`,要求显式 `std::ref()` |
| Typed Property | `Ref` → `RefWrap<T>::typed()` | 直接传原始 `php::Ref` |
| PHP 数组元素 | `Ref` → `RefWrap<T>::typed()` | 直接传原始 `php::Ref` |
| `php::Var/php::Ref` | 运行时校验后经 `RefWrap<T>` | 直接传 `php::Ref` |
## 性能模型
不能把正确性建立在“GCC 可能优化掉”之上。生成结构本身应让常见静态路径保持零成本,
编译器优化只负责消除栈对象和内联 helper。
| 路径 | 额外堆分配 | 预期开销 |
| --- | --- | --- |
| typed local -> TypePHP typed-ref 参数 | 0 | 直接 `T&`,与普通 C++ 调用一致 |
| typed-ref local/参数 -> TypePHP typed-ref 参数 | 0 | 直接转发相同 `T&` |
| local typed alias | 0 | C++ reference,优化后通常不占独立存储 |
| 已存在 Zend reference -> typed-ref | 0 | 栈上 wrapper/代理和引用计数操作 |
| property/array item 首次变成 Zend reference | 通常 1 | Zend 建立 `zend_reference`,PHP 自身也需要 |
| typed local -> 动态 Zend 引用调用 | 每个引用根通常 1 | 必须构造真实 `zend_reference`,明确慢路径 |
最后一项不能用栈上的伪造 `zend_reference` 替代。ZendVM、用户函数或扩展可能增加
引用计数并暂存该 reference;把 refcounted 对象放在 C++ 栈上会产生释放非法地址或悬空
引用。该分配只在用户显式 `std::ref($typedLocal)` 进入动态调用时发生,不得污染普通调用。
### 生成代码约束
- `RefWrap` 是 header-only、非虚类,不使用 `std::function`、`std::vector` 或 type erasure;
- wrapper、tuple/array 与立即执行 lambda 只使用栈存储;
- 同一调用按引用根去重,避免重复建立 `zend_reference`;
- 成功主路径不创建错误消息或临时字符串,异常检查放入 `UNEXPECTED` 冷分支;
- 不在循环或函数级缓存临时 Zend reference,避免引用身份错误和逃逸;
- 普通静态 typed-ref 调用不得生成方向判断、commit 或 refcount 操作;
- `Bool` 代理只用于 Zend bridge,native `Bool& -> Bool&` 仍是直接调用。
### 验证标准
重构完成后不仅运行功能测试,还要检查生成物:
1. 对纯 native typed-ref micro benchmark 生成 `-O3 -S` 汇编,确认 wrapper 和 lambda
不存在、调用可正常 inline;
2. 统计静态调用循环的 `malloc/emalloc` 次数,必须为 0;
3. 分别 benchmark `Int/Float/Bool/Str/Array` 的直接调用、property/array bridge 和动态
`std::ref()`;
4. 比较 `-O0` 生成 C++ 的尺寸,避免每个调用点展开大段异常处理;公共慢路径放在 PHPX
非模板 helper 中;
5. 使用 ASan/Valgrind 验证异常、逃逸检测和多别名调用没有泄漏或悬空引用。
目标不是让动态 Zend bridge 等同于直接 C++ 调用,而是保证零成本静态主路径,并把不可
避免的装箱和堆分配限制在用户显式选择的动态边界。
## 实施顺序
1. 增加五种类型及类型帮助函数,但暂不改变生成代码;
2. 将明确类型的引用参数 ABI 改为 `T&`,补齐 wrapper 入口;
3. 支持 typed local 的静态直接调用;
4. 实现属性、静态属性、数组元素到 `T&` 的 `RefWrap`;
5. 实现 typed local 到动态 `php::Ref` 的 `RefWrap` 与异常安全写回;
6. 最后支持受限的一次性局部 alias,并让 SSA/控制流检查拒绝重绑定。
每一步都必须同时覆盖命名函数、方法、构造函数、named arguments、静态调用、
动态调用、Closure、异常路径,以及实例/静态 Typed Property 和数组元素。
## PHP 8.5 与 C++17 验证结果
实际探针得到以下结论:
- PHP 按从左到右顺序求值调用参数;内层引用调用的修改在下一个参数求值前可见;
- 引用调用修改值后再抛异常,修改仍然保留;
- `$a/$b/$d` 在 `$b =& $a; $d =& $b` 后拥有相同的 `ReflectionReference` ID;
- `$c = $b` 得到独立值,后续修改 `$c` 不影响 `$a/$b/$d`;
- PHP 严格模式允许把普通 `int` 变量传给 `float&`,并把调用方变量改成 `float`。
这与固定 `Int` 存储冲突,TypePHP typed-ref 应拒绝这种跨类型引用转换;
- Typed Property 创建的 reference 保留类型约束,不兼容写入立即抛出 TypeError;
- C++17 普通参数顺序未指定;列表初始化从左到右;放在外层参数中的临时 bridge
可能直到外层函数返回后才析构;
- zval 的 `Int/Float` 有可寻址 payload,`Bool` 没有。

@ -977,7 +977,7 @@ class CompilerBase implements PropertyAccessContext
return $this->getPlatform()->removeCommonPrefix($short, $long); return $this->getPlatform()->removeCommonPrefix($short, $long);
} }
protected function getVarType(string $name): string protected function getRawVarType(string $name): string
{ {
if ($this->hasLocalVar($name)) { if ($this->hasLocalVar($name)) {
return $this->context->localVars[$name]; return $this->context->localVars[$name];
@ -989,6 +989,16 @@ class CompilerBase implements PropertyAccessContext
return Type::VAR; return Type::VAR;
} }
/**
* Return the value type visible to expressions. A native C++ reference has
* the same operators and assignment rules as its referenced value; only
* ABI/binding code should inspect getRawVarType().
*/
protected function getVarType(string $name): string
{
return Type::getReferencedType($this->getRawVarType($name));
}
/** /**
* Resolve the ClassDef for an object expression (variable or $this). * Resolve the ClassDef for an object expression (variable or $this).
*/ */
@ -4984,7 +4994,9 @@ class CompilerBase implements PropertyAccessContext
if ($toType === Type::VAR or $fromType === Type::VAR) { if ($toType === Type::VAR or $fromType === Type::VAR) {
return true; return true;
} }
// References currently carry no type information, so treat them as var. $toType = Type::getReferencedType($toType);
$fromType = Type::getReferencedType($fromType);
// Dynamic references carry no compile-time target type, so treat them as var.
if ($toType === Type::REF or $fromType === Type::REF) { if ($toType === Type::REF or $fromType === Type::REF) {
return true; return true;
} }

Loading…
Cancel
Save