diff --git a/CHANGELOG.md b/CHANGELOG.md index c80e4722..76977a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 applies to native scalars, strings, arrays, objects, streams, high-precision -values, and `std` containers. Zend references are untyped, so allowing one to -alias fixed C++ storage could corrupt the variable's type. Use `std::any()` -before reference operations and explicitly convert the result back afterward. +values, and `std` containers. A reference made from an ordinary local has no +Zend type source, so exposing fixed C++ storage through it could corrupt the +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 `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 整数扩展语义的文件添加新兼容模式。 固定存储的局部变量不再允许转换为 PHP 引用,包括原生标量、字符串、数组、对象、 -stream、高精度值和 `std` 容器。Zend 引用没有类型约束,若允许其指向固定 C++ 存储, -可能破坏变量类型。需要引用操作时先使用 `std::any()`,操作完成后再显式转换回目标类型。 +stream、高精度值和 `std` 容器。普通局部变量产生的引用没有 Zend type source,若允许 +其指向固定 C++ 存储,可能破坏变量类型。Typed Property 仍然可以取引用:Zend 会把属性 +元数据记录为 type source;PHP 数组元素也仍是可取引用的动态槽位。局部变量需要引用 +语义时先使用 `std::any()`,操作完成后再显式转换回目标类型。 TypePHP 始终使用严格类型,项目源码不再需要 `declare(strict_types=1)`。该声明仍作为 冗余的 PHP 兼容语法被接受,而 `strict_types=0` 会被拒绝。 diff --git a/docs/en/INCOMPATIBLE_PHP_FEATURES.md b/docs/en/INCOMPATIBLE_PHP_FEATURES.md index 818a5b99..9aae76e3 100644 --- a/docs/en/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/en/INCOMPATIBLE_PHP_FEATURES.md @@ -105,13 +105,15 @@ incompatible with or more restrictive than standard PHP. properties. - A local with fixed storage (`Int`, `Float`, `Bool`, `Str`, `Array`, `Object`, `Stream`, high-precision values, or a `std` container) cannot be made into a - PHP reference. Zend references are untyped and could replace such storage - with an incompatible value. Objects, streams, typed objects, and `std` - containers already use handle/reference-like value semantics, so adding a - PHP reference to the local variable is unnecessary as well. Initialize the - value with `std::any()` when PHP reference semantics are required, then - convert it back explicitly with a keyword such as `toArray()` or - `toString()`. + PHP reference. An ordinary local reference has no Zend type source and could + replace such storage with an incompatible value. Objects, streams, typed + objects, and `std` containers already use handle/reference-like value + semantics, so adding a PHP reference to the local variable is unnecessary as + well. Typed object/static properties remain reference-capable because Zend + attaches their property metadata to the reference; PHP array elements remain + 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 dynamic dispatch and cannot use the native call path. diff --git a/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md b/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md index a41e0db8..46ef3123 100644 --- a/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md @@ -49,11 +49,12 @@ - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。 - `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。 - 采用固定存储的局部变量(`Int`、`Float`、`Bool`、`Str`、`Array`、`Object`、 - `Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。Zend 引用没有类型约束, - 可能把固定存储替换成不兼容的值。对象、stream、typed object 与 `std` 容器本身 - 已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。确需 PHP 引用 - 语义时,应使用 `std::any()` 初始化,完成引用操作后再通过 `toArray()`、 - `toString()` 等关键词显式转换回来。 + `Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。普通局部引用没有 Zend + type source,可能把固定存储替换成不兼容的值。对象、stream、typed object 与 + `std` 容器本身已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。 + Typed Property 仍可取引用,Zend 会将属性元数据附加到引用上;PHP 数组元素仍是 + 可取引用的动态槽位。局部变量确需 PHP 引用语义时,应使用 `std::any()` 初始化, + 完成引用操作后再通过 `toArray()`、`toString()` 等关键词显式转换回来。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。 ## 对象模型 diff --git a/docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md b/docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md new file mode 100644 index 00000000..39e28d3e --- /dev/null +++ b/docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md @@ -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` 在栈上把 +`php::Ref` 转成 `T&`: + +```cpp +php::Ref property = /* attrRef() */; +php::RefWrap 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` 为 typed local 创建临时 `php::Ref`: + +```cpp +php::Int value = 1; +php::RefWrap 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` 有两种互斥状态: + +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 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` | +| 动态调用 `std::ref($property/arrayItem)` | 直接传原始 Zend reference | + +## 调用分类 + +| 实参 | 静态 typed-ref 调用 | 动态调用/Closure | +| --- | --- | --- | +| typed local | 直接传 `T&` | `RefWrap`,要求显式 `std::ref()` | +| typed-ref local/parameter | 直接传 `T&` | `RefWrap`,要求显式 `std::ref()` | +| Typed Property | `Ref` → `RefWrap::typed()` | 直接传原始 `php::Ref` | +| PHP 数组元素 | `Ref` → `RefWrap::typed()` | 直接传原始 `php::Ref` | +| `php::Var/php::Ref` | 运行时校验后经 `RefWrap` | 直接传 `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` 没有。 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 1527879c..66f0389b 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -977,7 +977,7 @@ class CompilerBase implements PropertyAccessContext return $this->getPlatform()->removeCommonPrefix($short, $long); } - protected function getVarType(string $name): string + protected function getRawVarType(string $name): string { if ($this->hasLocalVar($name)) { return $this->context->localVars[$name]; @@ -989,6 +989,16 @@ class CompilerBase implements PropertyAccessContext 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). */ @@ -4984,7 +4994,9 @@ class CompilerBase implements PropertyAccessContext if ($toType === Type::VAR or $fromType === Type::VAR) { 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) { return true; }