diff --git a/CHANGELOG.md b/CHANGELOG.md index 76977a5a..500c5ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,14 +25,21 @@ For example, `$i = 100` is now permanently an integer local. Reusing `$i` as a Projects must remove `use native_types` and add the new compatibility mode only 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. 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. +Fixed `int`, `float`, `bool`, `string`, and `array` locals now use typed native +references. A stable local alias and an exact by-reference parameter on a +statically resolved TypePHP call lower directly to C++ `T&`; this path does not +box the value or allocate a Zend reference. Bindings must be unconditional, +one-time, function-local, and non-escaping. Rebinding, `unset`, reference +capture/return, and storing such a reference into PHP storage are rejected. + +Dynamic calls and Closures retain the existing Zend reference path and require +explicit `std::ref()` / `toRef()`. A call-scoped bridge validates the value on +write-back and rejects an escaping temporary reference. Use `std::any()` for +unrestricted PHP reference identity. Fixed object/resource/stream, +high-precision, Native/typed-object, Box, and `std`-container locals remain +non-referenceable because those types already carry identity/handle semantics. +Typed properties and PHP array elements continue to use Zend references and +their normal type-source behavior. TypePHP is always strict. Project sources no longer need `declare(strict_types=1)`; the directive remains accepted as a redundant PHP @@ -100,11 +107,16 @@ should review the change log and run their full test suite before upgrading. `$i = std::any(100)` 或在文件中声明 `use varint_types`。升级项目必须删除 `use native_types`,并且只为真正依赖 Zend 整数扩展语义的文件添加新兼容模式。 -固定存储的局部变量不再允许转换为 PHP 引用,包括原生标量、字符串、数组、对象、 -stream、高精度值和 `std` 容器。普通局部变量产生的引用没有 Zend type source,若允许 -其指向固定 C++ 存储,可能破坏变量类型。Typed Property 仍然可以取引用:Zend 会把属性 -元数据记录为 type source;PHP 数组元素也仍是可取引用的动态槽位。局部变量需要引用 -语义时先使用 `std::any()`,操作完成后再显式转换回目标类型。 +固定 `int`、`float`、`bool`、`string`、`array` 局部变量现在使用强类型原生引用。 +稳定的局部别名,以及静态可解析 TypePHP 调用上的精确引用参数,会直接生成 C++ `T&`, +不装箱、不创建 Zend reference。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、 +`unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。 + +动态调用与 Closure 继续使用既有 Zend reference 路径,并要求显式使用 `std::ref()` / +`toRef()`。调用级 bridge 在返回时检查类型并拒绝临时引用逃逸;需要完整 PHP 引用身份时 +应使用 `std::any()`。固定 object/resource/stream、高精度值、Native/typed object、Box +和 `std` 容器仍然禁止取引用,因为这些类型本身已有 identity/handle 语义。Typed +Property 与 PHP 数组元素继续使用 Zend reference 及其 type source 约束。 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 9aae76e3..2d2e0bbb 100644 --- a/docs/en/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/en/INCOMPATIBLE_PHP_FEATURES.md @@ -103,17 +103,25 @@ incompatible with or more restrictive than standard PHP. used explicitly. - `std::ref()` / `toRef()` only accept variables, array elements, or object 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. 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()`. +- Fixed `int`, `float`, `bool`, `string`, and `array` locals support a restricted + native-reference model. A one-time top-level binding such as `$alias =& $value` + becomes a C++ `T&`, and an exact `int/string/float/bool/array &$arg` on a + statically resolved TypePHP call also uses `T&` without boxing or allocating a + Zend reference. Rebinding, conditional/loop-local first binding, `unset`, + by-reference Closure capture, returning the local by reference, or storing the + reference in a property/array/global is rejected because the C++ reference may + not escape or change its target. +- Dynamic calls and Closure calls still require explicit `std::ref()` / `toRef()`. + TypePHP creates a call-scoped Zend reference, validates its type on write-back, + and reports an error if dynamic code retains it beyond the call. Code requiring + unrestricted PHP reference identity should initialize the local with + `std::any()` and use the existing `php::Var`/`php::Ref` path. +- Fixed object, resource/stream, high-precision, Native/typed-object, Box, and + `std`-container locals cannot be referenced. These values already have + handle/reference-like semantics, while rebinding their statically typed local + slot would weaken the type system. Typed object/static properties remain + reference-capable because Zend attaches property type sources; PHP array + elements remain dynamic reference-capable slots. - 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 46ef3123..4861940b 100644 --- a/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md @@ -48,13 +48,20 @@ - 引用赋值不支持从复杂静态属性表达式建立引用。 - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。 - `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。 -- 采用固定存储的局部变量(`Int`、`Float`、`Bool`、`Str`、`Array`、`Object`、 - `Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。普通局部引用没有 Zend - type source,可能把固定存储替换成不兼容的值。对象、stream、typed object 与 - `std` 容器本身已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。 - Typed Property 仍可取引用,Zend 会将属性元数据附加到引用上;PHP 数组元素仍是 - 可取引用的动态槽位。局部变量确需 PHP 引用语义时,应使用 `std::any()` 初始化, - 完成引用操作后再通过 `toArray()`、`toString()` 等关键词显式转换回来。 +- 固定 `int`、`float`、`bool`、`string`、`array` 局部变量支持受限的原生引用模型。 + `$alias =& $value` 这类函数顶层的一次性绑定会生成 C++ `T&`;静态可解析的 TypePHP + 调用中,精确的 `int/string/float/bool/array &$arg` 同样直接传递 `T&`,不装箱也不 + 创建 Zend reference。由于 C++ 引用不能改绑或逃逸,条件/循环内首次绑定、重新绑定、 + `unset`、Closure 按引用捕获、按引用返回局部变量,以及把引用保存到属性、数组或全局 + 槽位均会在编译期拒绝。 +- 动态函数、动态方法和 Closure 调用仍须显式使用 `std::ref()` / `toRef()`。TypePHP + 为本次调用建立 Zend reference,返回时校验类型并写回;动态代码若把临时引用保留到 + 调用之外会得到明确错误。需要完整 PHP 引用身份时,应以 `std::any()` 初始化局部变量, + 继续使用既有 `php::Var` / `php::Ref` 动态路径。 +- 固定 object、resource/stream、高精度值、Native/typed object、Box 与 `std` 容器局部 + 变量禁止取引用。这些值本身已有句柄或引用式语义,对其静态局部槽位改绑只会削弱类型 + 系统。Typed Property 仍可取引用,Zend 会附加属性 type source;PHP 数组元素仍是 + 可取引用的动态槽位。 - 带 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 index 39e28d3e..385b1d84 100644 --- a/docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md +++ b/docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md @@ -43,7 +43,6 @@ php::Int &alias = value; - `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`。 @@ -108,16 +107,21 @@ 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`,因此不会破坏属性类型。 +构造时必须验证当前值类型。跨 Zend 边界的性能不属于 typed-ref 的零成本目标,因此 +五种类型统一使用隔离代理,不直接暴露 `zend_reference` 内部 payload。这样即使原始 +reference 在 typed-ref 函数重入 ZendVM 时被其他动态别名改成另一种类型,也不会让 +`T&` 指向已经失效的 zval union member。 + +调用结束时比较入口快照、原始 reference 和代理:只有一侧发生修改时执行安全写回; +双方发生不同修改时抛出冲突错误,不能静默覆盖。`Array` 还必须比较 HashTable 身份, +因为“返回数组元素引用”会改变引用拓扑,即使数组值比较结果完全相同。数组代理使用 +独立副本,避免 persistent/default HashTable 与 request 写路径混用;字符串代理也必须 +持有 request-owned 存储,不能原地修改 persistent string。 ## typed-ref 转换为动态 PHP 引用 动态函数、动态方法和 Closure 的引用签名在编译期不可确定。调用者显式使用 -`std::ref()` 时,`php::RefWrap` 为 typed local 创建临时 `php::Ref`: +`std::ref()` 时,`php::RefWrap` 为 typed local 提供真实的 `php::Ref`: ```cpp php::Int value = 1; @@ -128,6 +132,8 @@ bridge.commit(); `commit()` 必须检查动态调用后的 zval 仍与 `T` 一致,然后写回 typed local; 类型不一致时抛出统一的 TypeError。不能依赖一个可能抛异常的析构函数完成检查。 +这是明确的动态慢路径,可以继续按调用创建 Zend reference,不为减少分配引入函数级 +缓存、复用状态或额外的引用身份管理。 桥接生命周期必须是“一次调用表达式”,不能是整个 PHP 语句。嵌套调用中,内层 调用返回后,必须在求值外层调用的下一个实参之前完成 `commit()`: @@ -136,10 +142,11 @@ bridge.commit(); outer(dynamicCall(std::ref($value)), nextArgument()); ``` -这里 `dynamicCall()` 的写回必须先于 `nextArgument()`。因此不能用语句级 -`beforeStmtLines/afterStmtLines` 实现,而应把 wrapper、调用、成功写回和异常写回 -统一放入一个立即执行的表达式 helper/lambda 中。多个 typed-ref 实参若指向同一变量, -必须复用同一个 bridge,不能创建内容互相覆盖的临时 reference。 +这里 `dynamicCall()` 的写回必须先于 `nextArgument()`。生成器可以使用立即执行 +lambda,也可以捕获并在独立 C++ block 中输出该内层调用的 +`beforeStmtLines/afterStmtLines`;关键是 wrapper、调用和成功/异常写回必须属于同一个 +求值边界。多个 typed-ref 实参若指向同一变量,必须复用同一个 bridge,不能创建内容 +互相覆盖的临时 reference。 调用抛出异常时,桥接层仍要按 PHP 的可观察语义处理已经发生的引用修改。生成代码 必须在重新抛出原异常前执行非破坏性的写回;若写回又发现类型错误,以类型错误替换 @@ -150,19 +157,19 @@ Zend 异常处理代码。 `RefWrap` 有两种互斥状态: -1. `T& -> php::Ref`:为动态调用创建临时 Zend reference,调用结束后验证并写回; +1. `T& -> php::Ref`:为动态调用创建 Zend reference,调用结束后验证并写回; 2. `php::Ref -> T&`:为静态 typed-ref 参数提供原生引用视图。 -建议内部状态明确表示为 `NativeToZend`、`ConstrainedZendToNative`、 -`DynamicZendToNative`,禁止用若干松散布尔值组合隐含方向。 +内部只区分 `T& -> php::Ref` 与 `php::Ref -> T&` 两种方向;成功提交后必须进入 +`committed` 状态,保证显式 `commit()` 与 `noexcept` 析构不会重复写回。 -| `T` | `php::Ref -> T&` 实现 | 是否需要写回代理 | +| `T` | `php::Ref -> T&` 代理 | 额外要求 | | --- | --- | --- | -| `Int` | 直接指向 `Z_LVAL_P(refval)` | 否 | -| `Float` | 直接指向 `Z_DVAL_P(refval)` | 否 | -| `Bool` | 栈上 `Bool` 代理 | 是 | -| `Str` | 指向 refval 的 indirect `Str` | 否 | -| `Array` | 指向 refval 的 indirect `Array` | 否 | +| `Int` | 栈上 `Int` | 精确 `IS_LONG` | +| `Float` | 栈上 `Float` | 精确 `IS_DOUBLE`;固定 `Int&` 不隐式改成 `Float&` | +| `Bool` | 栈上 `Bool` | `IS_TRUE/IS_FALSE` | +| `Str` | request-owned `Str` | 禁止原地修改 persistent string | +| `Array` | 独立 `Array` 副本 | 同时检测内容与 HashTable/引用拓扑变化 | `Bool` 不能直接映射的原因来自 Zend ABI:布尔值由 zval 的 `IS_TRUE/IS_FALSE` 类型标签表达,没有独立的 `bool` payload。 @@ -171,15 +178,10 @@ Zend 异常处理代码。 可能还有其他动态别名;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` 推断其以后不会改变。 +因此 `php::Ref -> T&` 不按 type source 选择不同内存模型。Typed Property 的 type +source 仍由 `Reference::operator=()` 在提交时执行 Zend 类型约束,但它不成为暴露裸 +payload 的理由。统一代理牺牲的是动态桥接路径的少量性能,换取更简单、可验证的生命 +周期规则;静态 TypePHP `T& -> T&` 主路径完全不经过这里。 ### 调用表达式边界 @@ -200,19 +202,18 @@ outer(first(), second()); // first/second 谁先执行未指定 ```cpp auto result = [&]() { php::RefWrap bridge(value); - try { - auto retval = dynamic_call(bridge.ref()); - bridge.commit(); - return retval; - } catch (zend_object *) { - bridge.commitAfterException(); - throw; - } + auto retval = dynamic_call(bridge.ref()); + bridge.commit(); + return retval; }(); ``` -这里 `commit()` 和 `bridge` 析构都发生在 lambda 产生结果之前。外层 -`VarList{inner(), next()}` 才能保证 `inner()` 写回先于 `next()`。 +正常返回由显式 `commit()` 完成全部校验;若调用抛出异常,C++ 栈展开会在离开 lambda +前执行 `noexcept` 析构,只提交类型兼容的既有修改并保留原异常。`commitAfterException()` +属于 `RefWrap` 私有实现,生成代码不直接调用它。这里 `commit()` 和 bridge 的析构都在 +lambda 产生结果之前。使用生成语句与局部 block 时必须保持相同边界。外层 +`VarList{inner(), next()}` 才能保证 `inner()` 写回先于 `next()`;不能依赖外层 +full-expression 的析构时机。 ### 异常路径 @@ -233,7 +234,7 @@ C++ 异常。这个规则必须集中在 helper 中,析构函数始终保持 ` ### 别名与逃逸 -同一次动态调用中,如果两个实参最终拥有相同引用根,必须复用一个 `RefWrap`: +同一次动态调用中,如果实参最终拥有相同引用根,必须复用一个 `RefWrap`: ```php $b =& $a; @@ -245,10 +246,14 @@ $callback(std::ref($b), std::ref($d)); 身份。编译器应按 `typedRefRoots` 对 bridge 去重。 动态 PHP 代码还可能把引用保存到全局、对象属性或 Closure 中,使其超过本次调用。 -栈上 native storage 无法安全支持这种逃逸。`RefWrap` 应在调用返回时检测临时 -`zend_reference` 是否出现额外持有者;检测到逃逸时抛出明确错误。该限制保证不会把 -已经失效的 C++ 地址暴露给 ZendVM。需要长期 PHP 引用身份的变量必须从一开始使用 -`std::any()`/`php::Ref`。 +栈上 native storage 无法安全支持这种逃逸。调用参数容器销毁后,`RefWrap` 应检查 +`zend_reference` 是否仍有额外持有者;检测到逃逸时抛出明确错误。该 reference 内只 +保存 native 值的副本,不保存 native 地址,因此不能形成悬空指针。需要长期 PHP 引用 +身份的变量必须从一开始使用 `std::any()`/`php::Ref`。 + +`Args`、位置参数 Array 与 named-argument Array 也会暂时增加 reference 引用计数。 +成功路径必须先清理这些编译器自有容器,再执行 `commit()`/逃逸检查;异常展开按 C++ +逆序析构,自有参数容器同样必须先于较早构造的 `RefWrap` 释放。 ## 局部引用约束 @@ -327,7 +332,8 @@ reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 ` 1. **固定类型不变**:typed-ref 的目标在整个生命周期内始终是同一个 C++ 类型; 2. **绑定不变**:一个 C++ reference 构造后永不重新绑定; -3. **根唯一**:引用链被压缩到唯一 root,同一次调用中一个 root 只创建一个 bridge; +3. **根唯一**:编译期已知的 typed-local 引用链被压缩到唯一 root,同一次调用中一个 + 已知 root 只创建一个 bridge; 4. **只求值一次**:属性 receiver、数组 key、动态 callable 和每个参数表达式都不得重复求值; 5. **PHP 求值顺序**:参数及嵌套调用按照 PHP 的从左到右可观察顺序完成; 6. **调用级提交**:bridge 在本次调用完成后、下一个表达式开始前提交; @@ -368,29 +374,72 @@ reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 ` ## 性能模型 -不能把正确性建立在“GCC 可能优化掉”之上。生成结构本身应让常见静态路径保持零成本, -编译器优化只负责消除栈对象和内联 helper。 +性能要求只覆盖编译期可确定的 TypePHP 原生调用与 typed-ref。不能把正确性建立在 +“GCC 可能优化掉”之上:该主路径在生成结构上就必须没有装箱、Zend reference 和堆 +分配,编译器优化只负责消除 C++ 引用别名并内联函数。`var-ref`、动态函数/方法和 +Closure 调用继续沿用现有 Zend 动态分配路径,不为这些慢路径增加复杂缓存。 + +例如下面的调用必须直接生成: + +```cpp +php::Int value = 1; +php::Int &alias = value; +php_increment(alias); // void php_increment(php::Int &) +``` + +即使使用 `-O0`,它也只传递一个地址,不申请堆内存;在函数体可见或启用 LTO 时, +GCC 通常会内联 `php_increment()` 并把 alias 完全消除。`php::Str&`、`php::Array&` +同样只传现有 wrapper 的地址,不产生 zval copy 或引用计数操作。 | 路径 | 额外堆分配 | 预期开销 | | --- | --- | --- | | 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/代理和引用计数操作 | +| Zend reference -> typed-ref | 不设零分配目标 | 隔离代理及安全提交;String/Array 可触发正常 COW | | property/array item 首次变成 Zend reference | 通常 1 | Zend 建立 `zend_reference`,PHP 自身也需要 | -| typed local -> 动态 Zend 引用调用 | 每个引用根通常 1 | 必须构造真实 `zend_reference`,明确慢路径 | +| typed local -> 动态 Zend 引用调用 | 每次调用、每个引用根通常 1 | 继续使用真实 `zend_reference`,明确慢路径 | -最后一项不能用栈上的伪造 `zend_reference` 替代。ZendVM、用户函数或扩展可能增加 +最后一项不能被 GCC 消除,也不能用栈上的伪造 `zend_reference` 替代。ZendVM、用户函数或扩展可能增加 引用计数并暂存该 reference;把 refcounted 对象放在 C++ 栈上会产生释放非法地址或悬空 -引用。该分配只在用户显式 `std::ref($typedLocal)` 进入动态调用时发生,不得污染普通调用。 +引用。该分配只在用户显式 `std::ref($typedLocal)` 进入动态调用时发生,不得污染普通 +原生调用;动态路径不承诺零分配。 + +### GCC 可优化边界 + +函数体可见或启用 LTO 时,以下开销可由 GCC/Clang 在 `-O2/-O3` 下完整消除: + +- `T&` alias 本身; +- 只捕获引用的立即执行 lambda; +- 固定大小 `php::VarList/std::array` 的栈对象; +- inline 的 `typed()`、无分支的静态调用转发; +- 未取地址、未逃逸的小型代理对象。 + +这些对象均不得被装入 `std::function`,也不得通过 `new`、`shared_ptr` 或动态容器保存; +否则 lambda/类型擦除可能引入堆分配,GCC 也不能保证消除。 + +下列开销具有 Zend 可观察语义,不能假定编译器会优化掉: + +- `ZVAL_NEW_REF`/`zend_reference` 分配与释放; +- String/Array 写入触发的 COW; +- zval 引用计数增减; +- 动态调用、type source 和逃逸检查; +- 异常实际发生时创建的 PHP/C++ 异常对象。 + +因此“零成本”只承诺静态 typed-ref 主路径。动态桥接不要求退化成裸 C++ 引用,也不为 +减少其分配而牺牲现有 PHPX 封装。`try/catch` 和 lambda 只出现在确实需要桥接的路径; +公共异常/逃逸处理应下沉到 out-of-line 冷函数,避免无谓扩大生成代码。 ### 生成代码约束 -- `RefWrap` 是 header-only、非虚类,不使用 `std::function`、`std::vector` 或 type erasure; -- wrapper、tuple/array 与立即执行 lambda 只使用栈存储; -- 同一调用按引用根去重,避免重复建立 `zend_reference`; +- `RefWrap` 是 header-only、非虚类,不使用 `std::function` 或 type erasure; +- 静态 typed-ref 主路径只使用 C++ `T&`,不得创建 wrapper 或 Zend reference; +- 动态/var-ref 路径可继续使用既有 `Args`、Zend reference 与动态内存分配,不为其性能 + 引入函数级缓存、跨调用复用或裸指针旁路; +- 同一调用中可静态证明同根的 typed-local 实参按引用根去重并共享 reference 身份; - 成功主路径不创建错误消息或临时字符串,异常检查放入 `UNEXPECTED` 冷分支; -- 不在循环或函数级缓存临时 Zend reference,避免引用身份错误和逃逸; +- 调用参数容器销毁后立即 `commit()`,不跨调用缓存或延迟同步值; +- 每次提交后验证没有逃逸; - 普通静态 typed-ref 调用不得生成方向判断、commit 或 refcount 操作; - `Bool` 代理只用于 Zend bridge,native `Bool& -> Bool&` 仍是直接调用。 @@ -400,15 +449,18 @@ reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 ` 1. 对纯 native typed-ref micro benchmark 生成 `-O3 -S` 汇编,确认 wrapper 和 lambda 不存在、调用可正常 inline; -2. 统计静态调用循环的 `malloc/emalloc` 次数,必须为 0; +2. 统计静态 typed-ref 调用循环的额外 `malloc/emalloc` 次数,必须为 0;动态 + `std::ref()` 只记录基线,不设零分配目标; 3. 分别 benchmark `Int/Float/Bool/Str/Array` 的直接调用、property/array bridge 和动态 `std::ref()`; 4. 比较 `-O0` 生成 C++ 的尺寸,避免每个调用点展开大段异常处理;公共慢路径放在 PHPX 非模板 helper 中; 5. 使用 ASan/Valgrind 验证异常、逃逸检测和多别名调用没有泄漏或悬空引用。 +6. 同时比较 `-O0` 与 `-O3`:正确性和分配上限不能依赖优化级别,`-O3` 只负责消除 + 栈上胶水;使用 `-fno-inline` 时也不得出现额外堆分配。 -目标不是让动态 Zend bridge 等同于直接 C++ 调用,而是保证零成本静态主路径,并把不可 -避免的装箱和堆分配限制在用户显式选择的动态边界。 +目标是保证零成本静态主路径。动态 Zend bridge 保持现有、安全、易维护的 PHPX 路径, +不将它的性能问题扩大为 typed-ref 设计复杂度。 ## 实施顺序 diff --git a/phpunit/code/typed-reference-argument-type-error.php b/phpunit/code/typed-reference-argument-type-error.php new file mode 100644 index 00000000..79696f42 --- /dev/null +++ b/phpunit/code/typed-reference-argument-type-error.php @@ -0,0 +1,9 @@ +value =& $value; +} diff --git a/phpunit/code/typed-reference-rebind.php b/phpunit/code/typed-reference-rebind.php new file mode 100644 index 00000000..b58010e2 --- /dev/null +++ b/phpunit/code/typed-reference-rebind.php @@ -0,0 +1,9 @@ + ['fixed-reference-std-container.php', 'php::StdVector']; } - public function testFixedStorageCannotBePassedToKnownReferenceParameter(): void + public function testFixedStorageUsesNativeReferenceForKnownTypedParameter(): void + { + $code = $this->compileFixture('fixed-reference-argument.php'); + + self::assertStringContainsString('void php_fixedreferenceargumenttarget(php::Str & value)', $code); + self::assertStringContainsString('php_fixedreferenceargumenttarget(value)', $code); + } + + public function testFixedStorageCanCreateOneTimeNativeLocalAlias(): void + { + $code = $this->compileFixture('fixed-reference-assignment.php'); + + self::assertStringContainsString('php::Array & reference = value;', $code); + self::assertStringNotContainsString('reference = value.toReference()', $code); + } + + public function testTypedReferenceAssignmentMismatchIsATypePhpError(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('variable $value of fixed type php::Str'); + $this->expectExceptionMessage( + 'Cannot re-assign `$reference` from `php::Str` to `php::Int`', + ); - $this->compileFixture('fixed-reference-argument.php'); + $this->compileFixture('typed-reference-assignment-type-error.php'); } - public function testFixedStorageCannotBeUsedAsReferenceAssignmentSource(): void + public function testTypedReferenceArgumentMismatchIsATypePhpError(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('variable $value of fixed type php::Array'); + $this->expectExceptionMessage( + 'Cannot pass value of type php::Int to reference parameter of type php::Float &', + ); - $this->compileFixture('fixed-reference-assignment.php'); + $this->compileFixture('typed-reference-argument-type-error.php'); } public function testFixedStorageCannotBeReturnedByReference(): void @@ -48,6 +68,14 @@ final class FixedReferenceStorageTest extends BaseTest $this->compileFixture('fixed-reference-return.php'); } + public function testTypedReferenceParameterCannotEscapeByReturn(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('variable $value of fixed type php::Int'); + + $this->compileFixture('typed-reference-parameter-return.php'); + } + public function testFixedStaticStorageCannotBeCapturedByReference(): void { $this->expectException(TestError::class); @@ -56,12 +84,13 @@ final class FixedReferenceStorageTest extends BaseTest $this->compileFixture('fixed-reference-static.php'); } - public function testFixedStorageCannotUseToRefKeyword(): void + public function testFixedStorageUsesBridgeForToRefAtKnownDynamicReferenceBoundary(): void { - $this->expectException(TestError::class); - $this->expectExceptionMessage('variable $value of fixed type php::Array'); + $code = $this->compileFixture('fixed-reference-to-ref.php'); - $this->compileFixture('fixed-reference-to-ref.php'); + self::assertStringContainsString('php::RefWrap', $code); + self::assertStringContainsString('.ref()', $code); + self::assertStringContainsString('.commit()', $code); } public function testExplicitAnyCanUseReferenceStorage(): void @@ -72,6 +101,57 @@ final class FixedReferenceStorageTest extends BaseTest self::assertStringContainsString('value.toReference()', $code); } + /** @dataProvider invalidTypedReferenceOperationProvider */ + public function testInvalidTypedReferenceOperationsFailDuringTypePhpCompilation( + string $fixture, + string $message, + ): void { + $this->expectException(TestError::class); + $this->expectExceptionMessage($message); + + $this->compileFixture($fixture); + } + + public static function invalidTypedReferenceOperationProvider(): iterable + { + yield 'conditional binding' => [ + 'typed-reference-conditional-bind.php', + 'A typed reference local must be bound in the top-level scope of the function', + ]; + yield 'rebind' => [ + 'typed-reference-rebind.php', + 'Cannot rebind fixed or typed reference variable `$alias`', + ]; + yield 'rebind root' => [ + 'typed-reference-root-rebind.php', + 'Cannot rebind fixed or typed reference variable `$first`', + ]; + yield 'unset alias' => [ + 'typed-reference-unset-alias.php', + 'Cannot unset typed reference variable `$alias`', + ]; + yield 'unset root' => [ + 'typed-reference-unset-root.php', + 'Cannot unset variable `$value` while typed references point to it', + ]; + yield 'property escape' => [ + 'typed-reference-property-escape.php', + 'Cannot create a reference to variable $value of fixed type php::Int', + ]; + yield 'array escape' => [ + 'typed-reference-array-escape.php', + 'Cannot create a reference to variable $value of fixed type php::Int', + ]; + yield 'typed object parameter' => [ + 'typed-reference-object-param.php', + 'References are only supported for int, string, float, bool, array, mixed, or union types', + ]; + yield 'typed object closure parameter' => [ + 'typed-reference-object-closure-param.php', + 'References are only supported for int, string, float, bool, array, mixed, or union types', + ]; + } + private function compileFixture(string $fixture): string { global $translator; diff --git a/phpunit/src/TypedReferenceTypeTest.php b/phpunit/src/TypedReferenceTypeTest.php new file mode 100644 index 00000000..dead47e3 --- /dev/null +++ b/phpunit/src/TypedReferenceTypeTest.php @@ -0,0 +1,33 @@ + Type::INT_REF, + Type::STR => Type::STR_REF, + Type::FLOAT => Type::FLOAT_REF, + Type::BOOL => Type::BOOL_REF, + Type::ARRAY => Type::ARRAY_REF, + ]; + + foreach ($types as $valueType => $referenceType) { + self::assertSame($referenceType, Type::getReferenceType($valueType)); + self::assertSame($valueType, Type::getReferencedType($referenceType)); + self::assertTrue(Type::isTypedRefType($referenceType)); + self::assertTrue(Type::isAnyRefType($referenceType)); + } + } + + public function testDynamicReferenceRemainsDistinctFromTypedReferences(): void + { + self::assertTrue(Type::isAnyRefType(Type::REF)); + self::assertFalse(Type::isTypedRefType(Type::REF)); + self::assertNull(Type::getReferenceType(Type::VAR)); + self::assertSame(Type::REF, Type::getReferencedType(Type::REF)); + } +} diff --git a/src/Analysis/SsaBuilder.php b/src/Analysis/SsaBuilder.php index e1de3059..6c8cb5e1 100644 --- a/src/Analysis/SsaBuilder.php +++ b/src/Analysis/SsaBuilder.php @@ -372,7 +372,7 @@ class SsaBuilder * @param SsaBlock[] $blocks Reference to the blocks array being built * @param SsaBlock $currentBlock The current block (already in $blocks) */ - private function splitStmtList(array $stmts, array &$blocks, SsaBlock &$currentBlock): void + private function splitStmtList(array $stmts, array &$blocks, SsaBlock $currentBlock): SsaBlock { for ($i = 0; $i < count($stmts); $i++) { $stmt = $stmts[$i]; @@ -414,7 +414,7 @@ class SsaBuilder // If statement: expand into condition block + body blocks + join if ($stmt instanceof Stmt\If_) { - $this->expandIfStmt($stmt, $blocks, $currentBlock, $i, $stmts); + $currentBlock = $this->expandIfStmt($stmt, $blocks, $currentBlock, $i, $stmts); continue; } @@ -431,6 +431,8 @@ class SsaBuilder // Regular statement: add to current block $currentBlock->stmts[] = $stmt; } + + return $currentBlock; } /** @@ -444,7 +446,7 @@ class SsaBuilder * * @param SsaBlock[] $blocks */ - private function expandIfStmt(Stmt\If_ $ifStmt, array &$blocks, SsaBlock &$currentBlock, int $stmtIndex, array $allStmts): void + private function expandIfStmt(Stmt\If_ $ifStmt, array &$blocks, SsaBlock $currentBlock, int $stmtIndex, array $allStmts): SsaBlock { // The if statement is the last statement in the current (condition) block $currentBlock->stmts[] = $ifStmt; @@ -473,7 +475,7 @@ class SsaBuilder $currentBlock = $this->newBlock(); $blocks[] = $currentBlock; } - $this->expandIfStmt($innerIf, $blocks, $currentBlock, 0, [$innerIf]); + $currentBlock = $this->expandIfStmt($innerIf, $blocks, $currentBlock, 0, [$innerIf]); } } if ($ifStmt->else) { @@ -514,6 +516,8 @@ class SsaBuilder if ($stmtIndex < count($allStmts) - 1) { $currentBlock = $joinBlock; } + + return $currentBlock; } /** diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 66f0389b..554875d7 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -764,8 +764,11 @@ class CompilerBase implements PropertyAccessContext if (isset($this->context->declaredObjects[$object])) { return $this->context->declaredObjects[$object]; } - if (isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object])) { - return $this->getObjectType($object); + if (isset($this->context->objects[$object])) { + return $this->context->objects[$object]; + } + if (isset($this->context->stableObjects[$object])) { + return $this->context->stableObjects[$object]; } return ''; } @@ -2475,7 +2478,10 @@ class CompilerBase implements PropertyAccessContext $remainingVariableUses[$name]--; // Only consume a local on its final occurrence. Globals and // statics outlive the function and must never be emptied. - if ($remainingVariableUses[$name] === 0 && $this->hasLocalVar($name)) { + if ($remainingVariableUses[$name] === 0 + && $this->hasLocalVar($name) + && !Type::isTypedRefType($this->getRawVarType($name)) + ) { $value = 'std::move(' . $value . ')'; } } @@ -5021,6 +5027,29 @@ class CompilerBase implements PropertyAccessContext $this->fatalError($left, "Cannot re-assign $varName from `{$fromType}` to `{$toType}`"); } + protected function checkTypedReferenceAssignExpr( + NodeAbstract $left, + string $referenceType, + string $fromType, + ): void { + $targetType = Type::getReferencedType($referenceType); + $fromType = Type::getReferencedType($fromType); + if ($fromType === Type::VAR || $fromType === Type::REF) { + return; + } + $compatible = $targetType === Type::FLOAT + ? ($fromType === Type::FLOAT || $fromType === Type::INT) + : $fromType === $targetType; + if ($compatible) { + return; + } + + $varName = $this->isVarExpr($left) + ? '`$' . $this->parseIdentifier($left) . '`' + : 'typed reference'; + $this->fatalError($left, "Cannot re-assign {$varName} from `{$fromType}` to `{$targetType}`"); + } + /** * Check a value against a composite PHP type when the value's static type * is precise enough to prove a mismatch. Composite declarations still use diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 84f4f544..6fa3e5f4 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -22,6 +22,29 @@ trait CompilationStateTrait $this->context->localVars[$name] = $type; } + protected function addTypedRefLocal(string $name, string $target, string $type): void + { + if (!\TypePhp\Type::isTypedRefType($type)) { + throw new \LogicException('Typed reference local requires a typed-ref type'); + } + $root = $this->context->typedRefRoots[$target] ?? $target; + $this->addLocalVar($name, $type); + $this->context->localVarInitializers[$name] = $target; + $this->context->typedRefBindings[$name] = $target; + $this->context->typedRefRoots[$name] = $root; + $this->context->typedRefAliases[$root][$name] = true; + } + + protected function isTypedRefLocal(string $name): bool + { + return isset($this->context->typedRefRoots[$name]); + } + + protected function isTypedRefRoot(string $name): bool + { + return isset($this->context->typedRefAliases[$name]); + } + protected function registerStdType(string $key): int { if (isset($this->stdTypeMap[$key])) { diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 90e8227c..0953ecdb 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -72,6 +72,14 @@ class FunctionContext public array $explicitNativeTypeVars = []; /** @var array C++ initializers folded into function-scope local declarations. */ public array $localVarInitializers = []; + /** @var array typed-ref local => directly referenced local. */ + public array $typedRefBindings = []; + /** @var array typed-ref local => canonical fixed-storage root. */ + public array $typedRefRoots = []; + /** @var array> canonical root => aliases. */ + public array $typedRefAliases = []; + /** @var list> active dynamic-call root => RefWrap variable scopes. */ + public array $typedRefBridgeScopes = []; public array $staticVars = []; public array $globalVars = []; @@ -129,6 +137,10 @@ class FunctionContext $this->localVars = []; $this->explicitNativeTypeVars = []; $this->localVarInitializers = []; + $this->typedRefBindings = []; + $this->typedRefRoots = []; + $this->typedRefAliases = []; + $this->typedRefBridgeScopes = []; $this->staticVars = []; $this->arguments = []; $this->immutableVars = []; @@ -184,6 +196,10 @@ class FunctionContext { $this->localVars = $localVars; $this->localVarInitializers = []; + $this->typedRefBindings = []; + $this->typedRefRoots = []; + $this->typedRefAliases = []; + $this->typedRefBridgeScopes = []; $this->tmpVarIndex = $tmpVarIndex; $this->declaredObjects = $declaredObjects; $this->nativeObjects = $nativeObjects; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index bfd14f6e..5c1c3359 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -1,14 +1,13 @@ assertCallArgumentLimit($callArgs); - $functionDef = $this->getFunction($nativeFunc); - $providedArgs = []; - $defaultArgs = []; - $sourceArgs = []; - $variadicArgCount = 0; - $hasNamedArg = false; - $argNameIndex = $this->getFunctionArgNameIndex($functionDef); - $variadicArgIndex = $this->getVariadicArgIndex($functionDef); - // Reorder the named arguments into their declared positions - foreach ($callArgs as $i => $arg) { - if ($this->isPlaceholderExpr($arg)) { - throw new PlaceHolder(); - } - if ($arg->name) { - $argName = $arg->name->name; - $k = $argNameIndex[$argName] ?? null; - if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) { - if ($k < $parameterOffset) { - $this->fatalError($arg, 'Named argument cannot target the extension receiver'); - } - $providedArgs[$k] = true; - $sourceArgs[] = [$k, null, $arg]; - } else { - if ($variadicArgIndex === null) { - $this->fatalError($arg, "Unknown named argument `{$argName}`"); + $this->context->typedRefBridgeScopes[] = []; + try { + $functionDef = $this->getFunction($nativeFunc); + $providedArgs = []; + $defaultArgs = []; + $sourceArgs = []; + $variadicArgCount = 0; + $hasNamedArg = false; + $argNameIndex = $this->getFunctionArgNameIndex($functionDef); + $variadicArgIndex = $this->getVariadicArgIndex($functionDef); + // Reorder the named arguments into their declared positions + foreach ($callArgs as $i => $arg) { + if ($this->isPlaceholderExpr($arg)) { + throw new PlaceHolder(); + } + if ($arg->name) { + $argName = $arg->name->name; + $k = $argNameIndex[$argName] ?? null; + if ($k !== null and ($variadicArgIndex === null or $k < $variadicArgIndex)) { + if ($k < $parameterOffset) { + $this->fatalError($arg, 'Named argument cannot target the extension receiver'); + } + $providedArgs[$k] = true; + $sourceArgs[] = [$k, null, $arg]; + } else { + if ($variadicArgIndex === null) { + $this->fatalError($arg, "Unknown named argument `{$argName}`"); + } + $sourceArgs[] = [$variadicArgIndex, $argName, $arg]; + $variadicArgCount++; } - $sourceArgs[] = [$variadicArgIndex, $argName, $arg]; + $hasNamedArg = true; + } elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) { + $sourceArgs[] = [$variadicArgIndex, null, $arg]; $variadicArgCount++; + } else { + $argIndex = $i + $parameterOffset; + $providedArgs[$argIndex] = true; + $sourceArgs[] = [$argIndex, null, $arg]; } - $hasNamedArg = true; - } elseif ($variadicArgIndex !== null and $i + $parameterOffset >= $variadicArgIndex) { - $sourceArgs[] = [$variadicArgIndex, null, $arg]; - $variadicArgCount++; - } else { - $argIndex = $i + $parameterOffset; - $providedArgs[$argIndex] = true; - $sourceArgs[] = [$argIndex, null, $arg]; - } - } - // Fill ABI holes first, but do not sort yet. User expressions must be - // lowered in source order; sorting raw AST arguments here would also - // reorder their side effects. - if ($hasNamedArg) { - $lastProvidedIndex = $providedArgs === [] - ? $parameterOffset - 1 - : max(array_keys($providedArgs)); - if ($deferTrailingDefaults && $variadicArgCount > 0) { - $lastProvidedIndex = $variadicArgIndex; - } - // Holes left between named arguments must be filled with default arguments - foreach ($functionDef->argInfoList as $k => $argInfo) { - if ($k < $parameterOffset) { - continue; - } - if ($variadicArgIndex !== null and $k === $variadicArgIndex) { - continue; + } + // Fill ABI holes first, but do not sort yet. User expressions must be + // lowered in source order; sorting raw AST arguments here would also + // reorder their side effects. + if ($hasNamedArg) { + $lastProvidedIndex = $providedArgs === [] + ? $parameterOffset - 1 + : max(array_keys($providedArgs)); + if ($deferTrailingDefaults && $variadicArgCount > 0) { + $lastProvidedIndex = $variadicArgIndex; } - if (!isset($providedArgs[$k])) { - // A Native virtual overload must omit a trailing default - // so the dynamically selected implementation supplies it. - // A named-argument hole before a later argument cannot be - // represented by a positional C++ overload without a - // presence mask, so reject that uncommon shape explicitly. - if ($deferTrailingDefaults && $k > $lastProvidedIndex) { + // Holes left between named arguments must be filled with default arguments + foreach ($functionDef->argInfoList as $k => $argInfo) { + if ($k < $parameterOffset) { continue; } - if ($deferTrailingDefaults) { - $this->fatalError( - reset($callArgs), - 'Named calls to Native virtual methods cannot skip an earlier optional parameter', - ); + if ($variadicArgIndex !== null and $k === $variadicArgIndex) { + continue; } - if (!$argInfo->hasDefaultValue()) { - $errorNode = null; - foreach ($callArgs as $a) { - if ($a instanceof Node\Arg && $a->name) { - $errorNode = $a; - break; + if (!isset($providedArgs[$k])) { + // A Native virtual overload must omit a trailing default + // so the dynamically selected implementation supplies it. + // A named-argument hole before a later argument cannot be + // represented by a positional C++ overload without a + // presence mask, so reject that uncommon shape explicitly. + if ($deferTrailingDefaults && $k > $lastProvidedIndex) { + continue; + } + if ($deferTrailingDefaults) { + $this->fatalError( + reset($callArgs), + 'Named calls to Native virtual methods cannot skip an earlier optional parameter', + ); + } + if (!$argInfo->hasDefaultValue()) { + $errorNode = null; + foreach ($callArgs as $a) { + if ($a instanceof Node\Arg && $a->name) { + $errorNode = $a; + break; + } } + $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + $this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argName . '` is missing default value'); } - $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); - $this->fatalError($errorNode ?? reset($callArgs), 'Named argument `' . $argName . '` is missing default value'); + // Defaults are resolved in the declaration scope. Re-parsing + // the original AST here would evaluate self/parent/private + // class constants in the caller's scope instead. + $defaultArgs[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k); } - // Defaults are resolved in the declaration scope. Re-parsing - // the original AST here would evaluate self/parent/private - // class constants in the caller's scope instead. - $defaultArgs[$k] = $this->genDefaultArgumentExpr($nativeFunc, $k); } } - } - // If the function only accepts a single variadic parameter and the call - // supplies no arguments, pass an empty array directly - if (count($sourceArgs) === 0 - and count($functionDef->argInfoList) === $parameterOffset + 1 - and $functionDef->argInfoList[$parameterOffset]->variadic) { - return $deferTrailingDefaults ? '' : '{}'; - } - - $resolvedArgs = []; - $variadicVar = null; - $callableName = $functionDef->displayName ?: $functionDef->getNamespacedName(); - - // PHP evaluates arguments left to right. A later argument that hoists - // captured statements while being lowered (an assignment, a call) - // would execute those side effects before an earlier plain-variable - // argument is read: `two($j, $j = 5)` must pass the old value of $j. - // Record the last such argument so every earlier by-value variable - // read can be snapshotted at its own argument position. - $lastHoistingSourceIndex = -1; - foreach ($sourceArgs as $sourceIndex => [, , $arg]) { - if ($arg instanceof Node\Arg && $this->shouldMaterializeOrderedOperand($arg->value)) { - $lastHoistingSourceIndex = $sourceIndex; - } - } - - // Evaluate every supplied argument in PHP source order. The resulting - // expressions/temporaries may then be rearranged safely for the native - // C++ ABI without changing observable call order. - foreach ($sourceArgs as $sourceIndex => [$sourceArgIndex, $variadicName, $arg]) { - if ($sourceIndex < $lastHoistingSourceIndex - && $arg instanceof Node\Arg - && !$arg->unpack - && $this->isSnapshotableVariableRead($arg->value) - ) { - $paramInfo = $sourceArgIndex === $variadicArgIndex - ? $functionDef->argInfoList[$variadicArgIndex] - : $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); - if ($paramInfo !== null && !$paramInfo->byRef) { - $snapshot = $this->parseOrderedOperand($arg->value, false, true); - $arg = clone $arg; - $arg->value = new Expr\Variable($snapshot, $arg->value->getAttributes()); + // If the function only accepts a single variadic parameter and the call + // supplies no arguments, pass an empty array directly + if (count($sourceArgs) === 0 + and count($functionDef->argInfoList) === $parameterOffset + 1 + and $functionDef->argInfoList[$parameterOffset]->variadic) { + return $deferTrailingDefaults ? '' : '{}'; + } + + $resolvedArgs = []; + $variadicVar = null; + $callableName = $functionDef->displayName ?: $functionDef->getNamespacedName(); + + // PHP evaluates arguments left to right. A later argument that hoists + // captured statements while being lowered (an assignment, a call) + // would execute those side effects before an earlier plain-variable + // argument is read: `two($j, $j = 5)` must pass the old value of $j. + // Record the last such argument so every earlier by-value variable + // read can be snapshotted at its own argument position. + $lastHoistingSourceIndex = -1; + foreach ($sourceArgs as $sourceIndex => [, , $arg]) { + if ($arg instanceof Node\Arg && $this->shouldMaterializeOrderedOperand($arg->value)) { + $lastHoistingSourceIndex = $sourceIndex; } } - if ($sourceArgIndex !== $variadicArgIndex) { - $argInfo = $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); - $resolvedArgs[$sourceArgIndex] = $this->getTypeConvertedArg( - $arg, - $argInfo, - $callableName, - $sourceArgIndex - ); - continue; - } - $argInfo = $functionDef->argInfoList[$variadicArgIndex]; - // A single unpacked by-value native array is already the ABI - // value. A by-reference variadic must still separate the source - // and turn every element into a reference before entering the - // callee, matching Zend's argument-unpacking semantics. - if (!$argInfo->byRef && $variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) { - $var = $this->parseIdentifier($arg->value); - if ($this->getVarType($var) === Type::ARRAY) { - $resolvedArgs[$variadicArgIndex] = $var; + // Evaluate every supplied argument in PHP source order. The resulting + // expressions/temporaries may then be rearranged safely for the native + // C++ ABI without changing observable call order. + foreach ($sourceArgs as $sourceIndex => [$sourceArgIndex, $variadicName, $arg]) { + if ($sourceIndex < $lastHoistingSourceIndex + && $arg instanceof Node\Arg + && !$arg->unpack + && $this->isSnapshotableVariableRead($arg->value) + ) { + $paramInfo = $sourceArgIndex === $variadicArgIndex + ? $functionDef->argInfoList[$variadicArgIndex] + : $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); + if ($paramInfo !== null && !$paramInfo->byRef) { + $snapshot = $this->parseOrderedOperand($arg->value, false, true); + $arg = clone $arg; + $arg->value = new Expr\Variable($snapshot, $arg->value->getAttributes()); + } + } + if ($sourceArgIndex !== $variadicArgIndex) { + $argInfo = $this->getArgInfo($arg, $nativeFunc, $sourceArgIndex); + $resolvedArgs[$sourceArgIndex] = $this->getTypeConvertedArg( + $arg, + $argInfo, + $callableName, + $sourceArgIndex + ); continue; } - } - $variadicVar ??= $this->addTmpVar(Type::ARRAY); - if ($arg->unpack) { - $method = $argInfo->byRef ? 'mergeReferences' : 'merge'; - $this->context->beforeStmtLines[] = $variadicVar . '.' . $method - . '(' . $this->parseArrayArg($arg) . ');'; - } elseif ($variadicName !== null) { - $value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex); - $method = $argInfo->byRef ? 'set' : 'setValue'; - $this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '(' - . $this->getLiteralString($variadicName) . ', ' . $value . ');'; - } else { - $value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex); - $method = $argInfo->byRef ? 'append' : 'appendValue'; - $this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '(' . $value . ');'; + $argInfo = $functionDef->argInfoList[$variadicArgIndex]; + // A single unpacked by-value native array is already the ABI + // value. A by-reference variadic must still separate the source + // and turn every element into a reference before entering the + // callee, matching Zend's argument-unpacking semantics. + if (!$argInfo->byRef && $variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) { + $var = $this->parseIdentifier($arg->value); + if ($this->getVarType($var) === Type::ARRAY) { + $resolvedArgs[$variadicArgIndex] = $var; + continue; + } + } + + $variadicVar ??= $this->addTmpVar(Type::ARRAY); + if ($arg->unpack) { + $method = $argInfo->byRef ? 'mergeReferences' : 'merge'; + $this->context->beforeStmtLines[] = $variadicVar . '.' . $method + . '(' . $this->parseArrayArg($arg) . ');'; + } elseif ($variadicName !== null) { + $value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex); + $method = $argInfo->byRef ? 'set' : 'setValue'; + $this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '(' + . $this->getLiteralString($variadicName) . ', ' . $value . ');'; + } else { + $value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex); + $method = $argInfo->byRef ? 'append' : 'appendValue'; + $this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '(' . $value . ');'; + } } - } - // Defaults have no caller-side evaluation. Add them after user - // arguments, then sort only the already-lowered values for the ABI. - foreach ($defaultArgs as $i => $defaultArg) { - $resolvedArgs[$i] = $defaultArg; - } - if ($variadicVar !== null) { - $resolvedArgs[$variadicArgIndex] = $variadicVar; - if ($functionDef->argInfoList[$variadicArgIndex]->byRef) { - // The aggregation array owns the second reference to every - // caller slot. Release it after the full PHP statement and - // also during C++ exception unwinding into a PHP catch block. - $cleanupGuard = $this->genTmpVarName(); - $this->context->beforeStmtLines[] = 'php::ArrayCleanupGuard ' . $cleanupGuard - . '{' . $variadicVar . '};'; - $this->context->afterStmtLines[] = $cleanupGuard . '.cleanup();'; + // Defaults have no caller-side evaluation. Add them after user + // arguments, then sort only the already-lowered values for the ABI. + foreach ($defaultArgs as $i => $defaultArg) { + $resolvedArgs[$i] = $defaultArg; + } + if ($variadicVar !== null) { + $resolvedArgs[$variadicArgIndex] = $variadicVar; + if ($functionDef->argInfoList[$variadicArgIndex]->byRef) { + // The aggregation array owns the second reference to every + // caller slot. Release it after the full PHP statement and + // also during C++ exception unwinding into a PHP catch block. + $cleanupGuard = $this->genTmpVarName(); + $this->context->beforeStmtLines[] = 'php::ArrayCleanupGuard ' . $cleanupGuard + . '{' . $variadicVar . '};'; + $this->context->afterStmtLines[] = $cleanupGuard . '.cleanup();'; + } } + ksort($resolvedArgs); + return implode(', ', $resolvedArgs); + } finally { + array_pop($this->context->typedRefBridgeScopes); } - ksort($resolvedArgs); - return implode(', ', $resolvedArgs); } protected function isReferenceArgument($funcName, $className, $argIndex): bool @@ -415,183 +418,216 @@ trait CallArgumentGenerator string $className = '', bool $separateNamedArgs = true, bool $forceArrayArgs = false, - bool $preserveExistingReferences = false - ): string - { + bool $preserveExistingReferences = false, + ): string { $this->assertCallArgumentLimit($args); - $list_args = []; - $arrayArgsVar = null; - $argsVar = null; - $namedArgsVar = null; - $namedArgs = []; - $hasNamedArg = false; - $hasUnpack = false; - - if ($forceArrayArgs) { - $this->ensureCallArrayArgs($arrayArgsVar, $list_args); - } + // Nested calls need independent bridge identities and commit points. + // Within one call, typed aliases that share a canonical native root + // must reuse one RefWrap so Zend observes one reference identity. + $this->context->typedRefBridgeScopes[] = []; + $afterStmtStart = count($this->context->afterStmtLines); + try { + $list_args = []; + $arrayArgsVar = null; + $argsVar = null; + $namedArgsVar = null; + $namedArgs = []; + $hasNamedArg = false; + $hasUnpack = false; + $lastHoistingArgIndex = -1; + + foreach ($args as $argIndex => $sourceArg) { + if ($sourceArg instanceof Node\Arg + && !$sourceArg->unpack + && $this->shouldMaterializeOrderedOperand($sourceArg->value) + ) { + $lastHoistingArgIndex = $argIndex; + } + } - foreach ($args as $i => $arg) { - if ($this->isPlaceholderExpr($arg)) { - throw new PlaceHolder(); + if ($forceArrayArgs) { + $this->ensureCallArrayArgs($arrayArgsVar, $list_args); } - if ($arg->unpack) { - if ($hasNamedArg) { - $this->fatalError($arg, 'Cannot use argument unpacking after named arguments'); + + foreach ($args as $i => $arg) { + if ($this->isPlaceholderExpr($arg)) { + throw new PlaceHolder(); } - $hasUnpack = true; - if (!$forceArrayArgs && $separateNamedArgs) { - $callArgs = $this->ensureCallArgs($argsVar, $list_args); - $this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');'; - if ($arg->getAttribute(self::ATTR_SCOPED_CALLBACK) === 'normalize-unpacked') { - $this->context->beforeStmtLines[] = 'php::normalizeCallableClass(' - . $callArgs . ', 0, ' . $this->getCallableScopeExpr() . ');'; + if ($arg->unpack) { + if ($hasNamedArg) { + $this->fatalError($arg, 'Cannot use argument unpacking after named arguments'); } - } else { - $arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args); - $this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');'; + $hasUnpack = true; + if (!$forceArrayArgs && $separateNamedArgs) { + $callArgs = $this->ensureCallArgs($argsVar, $list_args); + $this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');'; + if ($arg->getAttribute(self::ATTR_SCOPED_CALLBACK) === 'normalize-unpacked') { + $this->context->beforeStmtLines[] = 'php::normalizeCallableClass(' + . $callArgs . ', 0, ' . $this->getCallableScopeExpr() . ');'; + } + } else { + $arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args); + $this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');'; + } + continue; } - continue; - } - if ($arg->name !== null) { - $hasNamedArg = true; - if (!$this->isIdExpr($arg->name)) { - $this->fatalError($arg, 'Named argument must be a string'); + if ($arg->name !== null) { + $hasNamedArg = true; + if (!$this->isIdExpr($arg->name)) { + $this->fatalError($arg, 'Named argument must be a string'); + } + if (array_key_exists($arg->name->name, $namedArgs)) { + $this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`"); + } + $namedArgs[$arg->name->name] = true; + $byRef = ($funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name)) + || ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg)); + if ($byRef) { + $this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false); + } + $value = ($byRef || $this->isStdRefCall($arg->value) || $this->isToRefCall($arg->value)) + ? $this->parseReferenceCallArgValue($arg) + : $this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex); + $value = $this->wrapScopedCallbackArg($arg, $value); + if ($separateNamedArgs) { + $namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar); + $this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; + } else { + $arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args); + $method = $forceArrayArgs ? 'setValue' : 'set'; + $this->context->beforeStmtLines[] = $arrayArgs . '.' . $method . '(' + . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; + } + continue; } - if (array_key_exists($arg->name->name, $namedArgs)) { - $this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`"); + if ($hasNamedArg) { + $this->fatalError($arg, 'Cannot use positional argument after named argument'); } - $namedArgs[$arg->name->name] = true; - $byRef = ($funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name)) + if ($hasUnpack) { + $this->fatalError($arg, 'Cannot use positional argument after argument unpacking'); + } + $byRef = ($funcName && $this->isReferenceArgument($funcName, $className, $i)) || ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg)); if ($byRef) { $this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false); } - $value = ($byRef || $this->isStdRefCall($arg->value) || $this->isToRefCall($arg->value)) - ? $this->parseReferenceCallArgValue($arg) - : $this->parseCallArgValue($arg); - $value = $this->wrapScopedCallbackArg($arg, $value); - if ($separateNamedArgs) { - $namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar); - $this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; - } else { - $arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args); - $method = $forceArrayArgs ? 'setValue' : 'set'; - $this->context->beforeStmtLines[] = $arrayArgs . '.' . $method . '(' - . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; + $scopedCallback = $arg->getAttribute(self::ATTR_SCOPED_CALLBACK); + if ($scopedCallback !== null) { + if ($this->isVarExpr($arg->value)) { + $name = $this->parseIdentifier($arg->value); + if (!$this->hasVar($name)) { + $this->fatalError($arg, 'Undefined variable `$' . $name . '`'); + } + } + $value = $this->wrapScopedCallbackArg( + $arg, + $this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex), + ); + $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); + continue; } - continue; - } - if ($hasNamedArg) { - $this->fatalError($arg, 'Cannot use positional argument after named argument'); - } - if ($hasUnpack) { - $this->fatalError($arg, 'Cannot use positional argument after argument unpacking'); - } - $byRef = ($funcName && $this->isReferenceArgument($funcName, $className, $i)) - || ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg)); - if ($byRef) { - $this->assertReadonlyPropertyReferenceForbidden($arg->value, $arg, false); - } - $scopedCallback = $arg->getAttribute(self::ATTR_SCOPED_CALLBACK); - if ($scopedCallback !== null) { if ($this->isVarExpr($arg->value)) { $name = $this->parseIdentifier($arg->value); + if ($byRef) { + $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); + continue; + } if (!$this->hasVar($name)) { $this->fatalError($arg, 'Undefined variable `$' . $name . '`'); } - } - $value = $this->wrapScopedCallbackArg($arg, $this->parseCallArgValue($arg)); - $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - if ($this->isVarExpr($arg->value)) { - $name = $this->parseIdentifier($arg->value); - if ($byRef) { - $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - if (!$this->hasVar($name)) { - $this->fatalError($arg, 'Undefined variable `$' . $name . '`'); - } - } elseif ($this->isPropertyFetch($arg->value)) { - if ($byRef) { - $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - if ($this->isVarExpr($arg->value->var)) { - $objectExpr = $this->parseIdentifier($arg->value->var); - if (!$this->hasVar($objectExpr)) { - $this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`'); + } elseif ($this->isPropertyFetch($arg->value)) { + if ($byRef) { + $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args, $forceArrayArgs); + continue; + } + if ($this->isVarExpr($arg->value->var)) { + $objectExpr = $this->parseIdentifier($arg->value->var); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`'); + } + } + } elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) { + $array = $this->parseIdentifier($arg->value->var); + if ($array === 'GLOBALS') { + $globalVar = $this->parseGlobalsArrayDimFetch($arg->value); + // Global variable passed as a by-reference argument + if ($byRef) { + $ref = $this->addTmpVar(Type::REF); + $this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();'; + $this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args, $forceArrayArgs); + } else { + $this->addPositionalCallArg( + $this->materializeCallArgValue($arg->value, $globalVar), + $arrayArgsVar, + $list_args, + $forceArrayArgs, + ); + } + continue; + } + if ($this->isVarExpr($arg->value->var) and !$this->hasVar($array)) { + $this->fatalError($arg, 'Undefined variable `$' . $array . '`'); } - } - } elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) { - $array = $this->parseIdentifier($arg->value->var); - if ($array === 'GLOBALS') { - $globalVar = $this->parseGlobalsArrayDimFetch($arg->value); - // Global variable passed as a by-reference argument if ($byRef) { - $ref = $this->addTmpVar(Type::REF); - $this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();'; - $this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args, $forceArrayArgs); - } else { - $this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args, $forceArrayArgs); + if ($arg->value->dim === null) { + $this->fatalError($arg, 'Array dimension must be a constant expression'); + } + $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args, $forceArrayArgs); + continue; } - continue; - } - if ($this->isVarExpr($arg->value->var) and !$this->hasVar($array)) { - $this->fatalError($arg, 'Undefined variable `$' . $array . '`'); - } - if ($byRef) { - if ($arg->value->dim === null) { - $this->fatalError($arg, 'Array dimension must be a constant expression'); + } elseif ($this->isReferenceWrapperCall($arg->value)) { + $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); + if ($this->isVarExpr($inner)) { + $name = $this->parseVariable($inner); + $arg->value = $inner; + $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); + continue; } - $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - } elseif ($this->isReferenceWrapperCall($arg->value)) { - $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); - if ($this->isVarExpr($inner)) { - $name = $this->parseVariable($inner); - $arg->value = $inner; - $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - $expr = $this->expandReferenceWrapperExpr($inner, $arg); - if ($expr !== null) { - $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args, $forceArrayArgs); - continue; - } - $this->fatalError($arg, 'The std::ref function only accepts a variable, array element, or object property'); - } else { - if ($byRef) { - if ($this->isScalar($arg->value)) { - $this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter'); + $expr = $this->expandReferenceWrapperExpr($inner, $arg); + if ($expr !== null) { + $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args, $forceArrayArgs); + continue; + } + $this->fatalError($arg, 'The std::ref function only accepts a variable, array element, or object property'); + } else { + if ($byRef) { + if ($this->isScalar($arg->value)) { + $this->fatalError($arg, 'The constants cannot be used as an argument for a reference-type parameter'); + } + $tmpRef = $this->genTmpVarName(); + $this->addLocalVar($tmpRef, Type::REF); + $this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';'; + $this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args, $forceArrayArgs); + continue; } - $tmpRef = $this->genTmpVarName(); - $this->addLocalVar($tmpRef, Type::REF); - $this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';'; - $this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args, $forceArrayArgs); - continue; } + $value = $this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex); + $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); } - $value = $this->parseCallArgValue($arg); - $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); - } - if ($argsVar !== null) { - return $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar; + if ($argsVar !== null) { + $result = $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar; + } elseif ($arrayArgsVar !== null) { + $result = $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar; + } else { + // VarList deduces the fixed argument count and owns contiguous + // Variant storage, which PHPX passes directly to Zend without a + // dynamic php::Args allocation. materializeCallArgValue() above + // ensures that ordinary values do not leave INDIRECT borrows in the + // list; explicit reference arguments remain references. + $callArgs = Symbol::varList() . '{' . implode(', ', $list_args) . '}'; + $result = $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; + } + $this->scheduleCallArgumentCleanup( + $afterStmtStart, + $argsVar, + $arrayArgsVar, + $namedArgsVar, + ); + return $result; + } finally { + array_pop($this->context->typedRefBridgeScopes); } - if ($arrayArgsVar !== null) { - return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar; - } - // VarList deduces the fixed argument count and owns contiguous - // Variant storage, which PHPX passes directly to Zend without a - // dynamic php::Args allocation. materializeCallArgValue() above - // ensures that ordinary values do not leave INDIRECT borrows in the - // list; explicit reference arguments remain references. - $callArgs = Symbol::varList() . '{' . implode(', ', $list_args) . '}'; - return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; } private function isExistingReferenceCallArg(Node\Arg $arg): bool @@ -642,11 +678,32 @@ trait CallArgumentGenerator if ($namedArgsVar === null) { $namedArgsVar = $this->genTmpVarName(); $this->context->beforeStmtLines[] = Type::ARRAY . ' ' . $namedArgsVar . ';'; - $this->context->afterStmtLines[] = $namedArgsVar . '.unset();'; } return $namedArgsVar; } + /** Release compiler-owned argument containers before typed-ref commit. */ + private function scheduleCallArgumentCleanup( + int $afterStmtStart, + ?string $argsVar, + ?string $arrayArgsVar, + ?string $namedArgsVar, + ): void { + $cleanup = []; + if ($argsVar !== null) { + $cleanup[] = $argsVar . '.clear();'; + } + if ($arrayArgsVar !== null) { + $cleanup[] = $arrayArgsVar . '.unset();'; + } + if ($namedArgsVar !== null) { + $cleanup[] = $namedArgsVar . '.unset();'; + } + if ($cleanup !== []) { + array_splice($this->context->afterStmtLines, $afterStmtStart, 0, $cleanup); + } + } + protected function addPositionalCallArg( string $value, ?string $arrayArgsVar, @@ -682,14 +739,25 @@ trait CallArgumentGenerator // Those statements are placed before the whole outer call and would // overtake an earlier Call left inside the initializer list. Complete // each direct Call in a temporary before lowering the next argument. - $expr = $arg->value instanceof Expr\FuncCall - || $arg->value instanceof Expr\MethodCall - || $arg->value instanceof Expr\StaticCall + $expr = $this->shouldMaterializeOrderedOperand($arg->value) ? $this->parseOrderedArg($arg) : $this->parseArg($arg); return $this->materializeCallArgValue($arg->value, $expr); } + /** Preserve PHP's left-to-right value snapshots when a later argument is hoisted. */ + private function parseOrderedDynamicCallArgValue( + Node\Arg $arg, + int $argIndex, + int $lastHoistingArgIndex, + ): string { + if ($argIndex < $lastHoistingArgIndex && $this->isSnapshotableVariableRead($arg->value)) { + $expr = $this->parseOrderedOperand($arg->value, false, true); + return $this->materializeCallArgValue($arg->value, $expr); + } + return $this->parseCallArgValue($arg); + } + protected function materializeCallArgValue(NodeAbstract $value, string $expr): string { // A Native property fetch is a typed C++ pointer, never an INDIRECT @@ -718,7 +786,8 @@ trait CallArgumentGenerator return !$this->isStdContainerExpr($value); } - return $value instanceof Expr\PropertyFetch; + return $value instanceof Expr\PropertyFetch + || $value instanceof Expr\StaticPropertyFetch; } protected function assertCallArgumentLimit(array $args): void @@ -844,9 +913,14 @@ trait CallArgumentGenerator // For a by-reference parameter, an undefined variable may be passed; // it is created immediately as a reference $this->addLocalVar($name, Type::REF); - } elseif ($this->getVarType($name) === Type::REF) { + } elseif ($this->getRawVarType($name) === Type::REF) { return '&' . $name; } else { + $rawType = $this->getRawVarType($name); + $valueType = Type::getReferencedType($rawType); + if (Type::getReferenceType($valueType) !== null) { + return '&' . $this->getDynamicTypedRefBridge($name, $valueType) . '.ref()'; + } $this->assertVariableReferenceStorage($arg->value, $arg, $name); // For a by-reference parameter, use a temporary variable as the reference // and replace the actual argument with it @@ -861,6 +935,30 @@ trait CallArgumentGenerator return '&' . $name; } + /** + * Bridge fixed native storage into one dynamic Zend call. Typed aliases + * sharing a canonical root reuse one wrapper within the current argument + * list, preserving PHP reference identity and preventing competing writes. + */ + private function getDynamicTypedRefBridge(string $name, string $valueType): string + { + $root = $this->context->typedRefRoots[$name] ?? $name; + $scopeIndex = array_key_last($this->context->typedRefBridgeScopes); + if ($scopeIndex === null) { + throw new \LogicException('Typed reference bridge requires an active call-argument scope'); + } + if (isset($this->context->typedRefBridgeScopes[$scopeIndex][$root])) { + return $this->context->typedRefBridgeScopes[$scopeIndex][$root]; + } + + $wrapper = $this->genTmpVarName(); + $this->context->typedRefBridgeScopes[$scopeIndex][$root] = $wrapper; + $this->context->beforeStmtLines[] = 'php::RefWrap<' . $valueType . '> ' + . $wrapper . '(' . $name . ');'; + $this->context->afterStmtLines[] = $wrapper . '.commit();'; + return $wrapper; + } + protected function parseArg(Node\Arg $arg): string { if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) { @@ -916,5 +1014,4 @@ trait CallArgumentGenerator } return $this->parseExpr($value); } - } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 3940d994..9826bf1f 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -175,6 +175,26 @@ trait ClosureGenerator 'By-reference variadic parameters are not supported on dynamic Closures', ); } + if ($param->byRef && $param->type !== null) { + [$paramType, $paramClass] = $this->resolveTypeDecl( + $param->type, + self::DECL_TYPE_OF_PARAM, + ); + if ($paramClass !== '' || in_array($paramType, [ + Type::OBJECT, + Type::STREAM, + Type::BOX, + Type::STD_ARRAY, + Type::STD_VECTOR, + Type::STD_MAP, + Type::STD_ORDERED_MAP, + ], true)) { + $this->fatalError( + $param, + 'References are only supported for int, string, float, bool, array, mixed, or union types', + ); + } + } } $tmpVar = $this->genTmpVarName(); diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index a500461c..03f703c8 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -46,11 +46,13 @@ trait TypeCheckGenerator protected function isStrictScalarType(string $type): bool { + $type = Type::getReferencedType($type); return in_array($type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR], true); } protected function strictScalarTypeName(string $type): string { + $type = Type::getReferencedType($type); return match ($type) { Type::INT => 'int', Type::FLOAT => 'float', @@ -62,6 +64,7 @@ trait TypeCheckGenerator protected function genStrictScalarCondition(string $valueExpr, string $type): string { + $type = Type::getReferencedType($type); return match ($type) { Type::INT => $valueExpr . '.isInt()', // PHP permits int values at a float boundary even in strict mode. diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 89e34e8c..a4069d2d 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -1654,24 +1654,35 @@ trait NativeClassSupportTrait $ordered = []; $visited = []; - $visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void { - $key = strtolower(ltrim($class->getNamespacedName(false), '\\')); - if (isset($visited[$key])) { - return; - } - $visited[$key] = true; - $parent = strtolower(ltrim($class->extends, '\\')); - if ($parent !== '' && isset($byName[$parent])) { - $visit($byName[$parent]); - } - $ordered[] = $class; - }; foreach ($classes as $class) { - $visit($class); + $this->appendNativeObjectClassInDeclarationOrder($class, $byName, $ordered, $visited); } return $ordered; } + /** + * @param array $byName + * @param list $ordered + * @param array $visited + */ + private function appendNativeObjectClassInDeclarationOrder( + ClassDef $class, + array $byName, + array &$ordered, + array &$visited, + ): void { + $key = strtolower(ltrim($class->getNamespacedName(false), '\\')); + if (isset($visited[$key])) { + return; + } + $visited[$key] = true; + $parent = strtolower(ltrim($class->extends, '\\')); + if ($parent !== '' && isset($byName[$parent])) { + $this->appendNativeObjectClassInDeclarationOrder($byName[$parent], $byName, $ordered, $visited); + } + $ordered[] = $class; + } + protected function genNativeObjectDeclarations(): string { $classes = $this->getNativeObjectClassesInDeclarationOrder(); diff --git a/src/NativeClass/NativeGlobalTypeResolver.php b/src/NativeClass/NativeGlobalTypeResolver.php index 293114d5..c5c61011 100644 --- a/src/NativeClass/NativeGlobalTypeResolver.php +++ b/src/NativeClass/NativeGlobalTypeResolver.php @@ -13,6 +13,12 @@ use PhpParser\Node; use PhpParser\NodeAbstract; use TypePhp\Entity\ClassDef; +final class NativeGlobalConstantVisitState +{ + /** @var array */ + public array $keys = []; +} + /** * Immutable class metadata used by the Native global pre-pass. * @@ -176,7 +182,7 @@ final class NativeGlobalTypeResolver return $expression->value; } - $visiting = []; + $visiting = new NativeGlobalConstantVisitState(); try { $value = $this->evaluateConstantExpression($expression, $scopeClass, $visiting, 0); } catch (\Throwable) { @@ -185,11 +191,10 @@ final class NativeGlobalTypeResolver return is_string($value) ? $value : null; } - /** @param array $visiting */ private function evaluateConstantExpression( NodeAbstract $expression, string $scopeClass, - array &$visiting, + NativeGlobalConstantVisitState $visiting, int $depth, ): mixed { if ($depth > 32 || !$expression instanceof Node\Expr) { @@ -198,7 +203,7 @@ final class NativeGlobalTypeResolver $evaluator = new ConstExprEvaluator(function (Node\Expr $node) use ( $scopeClass, - &$visiting, + $visiting, $depth, ): mixed { if ($node instanceof Node\Expr\ConstFetch) { @@ -215,10 +220,10 @@ final class NativeGlobalTypeResolver } if (isset($this->globalConstantExpressions[$name])) { $key = 'global:' . $name; - if (isset($visiting[$key])) { + if (isset($visiting->keys[$key])) { throw new \RuntimeException('Circular constant expression'); } - $visiting[$key] = true; + $visiting->keys[$key] = true; try { return $this->evaluateConstantExpression( $this->globalConstantExpressions[$name], @@ -227,7 +232,7 @@ final class NativeGlobalTypeResolver $depth + 1, ); } finally { - unset($visiting[$key]); + unset($visiting->keys[$key]); } } if (defined($name)) { @@ -255,10 +260,10 @@ final class NativeGlobalTypeResolver throw new \RuntimeException('Class constant has no static expression'); } $key = 'class:' . $classKey . '::' . $constant; - if (isset($visiting[$key])) { + if (isset($visiting->keys[$key])) { throw new \RuntimeException('Circular class constant expression'); } - $visiting[$key] = true; + $visiting->keys[$key] = true; try { return $this->evaluateConstantExpression( $constantDefinition->valueExpr, @@ -267,7 +272,7 @@ final class NativeGlobalTypeResolver $depth + 1, ); } finally { - unset($visiting[$key]); + unset($visiting->keys[$key]); } } $parent = $this->parents[$classKey] ?? null; diff --git a/src/Optimizer/SsaPropOptimizer.php b/src/Optimizer/SsaPropOptimizer.php index 09338bd3..faf6d00c 100644 --- a/src/Optimizer/SsaPropOptimizer.php +++ b/src/Optimizer/SsaPropOptimizer.php @@ -459,10 +459,12 @@ trait SsaPropOptimizer $rightProp = $this->getPropNameOfObj($node->expr, $objName); if ($rightProp !== null) { - // $ref = &$o->prop changes the slot to a reference. Earlier - // optimized accesses remain safe only if the property is not - // touched again afterward. - $events[] = ['kind' => 'danger', 'prop' => $rightProp]; + // $ref = &$o->prop changes the zval slot itself from the + // declared scalar into IS_REFERENCE. A C++ reference hoisted + // from any earlier access would then alias the zend_reference + // pointer bits rather than the referenced scalar. Disable + // hoisting for this property for the complete function. + $events[] = ['kind' => 'danger_always', 'prop' => $rightProp]; $this->collectPropEventsInDynamicParts($node->expr, $objName, $events); } else { $this->collectPropEvents($node->expr, $objName, $events); @@ -475,7 +477,9 @@ trait SsaPropOptimizer && $node->args[0] instanceof Node\Arg) { $propName = $this->getPropNameOfObj($node->args[0]->value, $objName); if ($propName !== null) { - $events[] = ['kind' => 'danger', 'prop' => $propName]; + // std::ref($o->prop) has the same slot-changing effect as an + // explicit reference assignment. + $events[] = ['kind' => 'danger_always', 'prop' => $propName]; $this->collectPropEventsInDynamicParts($node->args[0]->value, $objName, $events); return; } diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 0a0c02dd..837f179c 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -64,7 +64,16 @@ trait ArrayExpressionTrait $this->indentLevel++; foreach ($items as $item) { $this->assertExprCanBeUsedAsValue($item->value, 'array value'); - $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); + // A call may bridge a fixed typed value through php::Ref. Finish + // the call and its RefWrap::commit() before evaluating the next + // array item, because PHP makes the writeback observable there: + // `[mutate($value), $value]`. C++ initializer ordering alone is + // insufficient when the commit is otherwise queued for the end + // of the containing PHP statement. + $value = $this->materializeRefReturnAsValue( + $item->value, + $this->parseOrderedOperand($item->value, false), + ); if ($item->key) { $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); @@ -290,7 +299,10 @@ trait ArrayExpressionTrait } $value = $this->convertToRef($item->value); } else { - $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); + $value = $this->materializeRefReturnAsValue( + $item->value, + $this->parseOrderedOperand($item->value, false), + ); } if ($item->unpack) { $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');'; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index ef6f9fbd..3dc784d0 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -631,15 +631,24 @@ trait AssignOpTrait $this->addLocalVar($var, Type::STD_ORDERED_MAP); return $this->parseStdOrderedMap($var, $right); } else { - $valueExpr = $this->parseStdCall($right); - $finalVarType = $right->getAttribute('nativeType') ?? Type::VAR; if (!$this->hasVar($var)) { + $valueExpr = $this->parseStdCall($right); + $finalVarType = $right->getAttribute('nativeType') ?? Type::VAR; $this->addLocalVar($var, $finalVarType); + if ($finalVarType !== Type::VAR) { + $this->context->explicitNativeTypeVars[$var] = true; + } + return $var . ' = ' . $valueExpr; } - if ($finalVarType !== Type::VAR) { + + // An existing local keeps its established storage and + // declared object constraint. std::any() deliberately + // erases only the RHS static type; returning directly + // here would bypass the normal assignment/type-check + // pipeline for typed objects and native scalars. + if (in_array($stdMethod, ['int', 'float', 'bool'], true)) { $this->context->explicitNativeTypeVars[$var] = true; } - return $var . ' = ' . $valueExpr; } } } elseif ($this->isVarExpr($right)) { @@ -669,7 +678,12 @@ trait AssignOpTrait $this->addLocalVar($var, $finalVarType); } else { $finalVarType = $this->getVarType($var); - $this->checkVarAssignExpr($left, $finalVarType, $type); + $rawVarType = $this->getRawVarType($var); + if (Type::isTypedRefType($rawVarType)) { + $this->checkTypedReferenceAssignExpr($left, $rawVarType, $type); + } else { + $this->checkVarAssignExpr($left, $finalVarType, $type); + } $declaredObjectClass = $this->getDeclaredObjectType($var); if (!$assigningNullToTypedObject && $finalVarType === Type::OBJECT @@ -736,6 +750,20 @@ trait AssignOpTrait : $rightExprType; return $var . ' = ' . $this->convertNativePropertyWriteExpr($propertyDef->type, $effectiveRightType, $rightExpr); } + $rawAssignedType = $this->getRawVarType($var); + if (Type::isTypedRefType($rawAssignedType) + && ($rightExprType === Type::VAR || $rightExprType === Type::REF) + ) { + $rightExpr = match (Type::getReferencedType($rawAssignedType)) { + Type::INT => 'php::toIntExact(' . $rightExpr . ')', + Type::FLOAT => 'php::toFloatExact(' . $rightExpr . ')', + Type::BOOL => 'php::toBoolExact(' . $rightExpr . ')', + Type::STR => 'php::toStringExact(' . $rightExpr . ')', + Type::ARRAY => 'php::toArrayExact(' . $rightExpr . ')', + default => throw new \LogicException('Unsupported typed reference assignment'), + }; + $rightExprType = Type::getReferencedType($rawAssignedType); + } $assignedExpr = $finalVarType === Type::VAR ? $rightExpr : $this->convertExprType($rightExpr, $finalVarType, $rightExprType); @@ -893,7 +921,7 @@ trait AssignOpTrait && $this->isZeroLiteral($node->expr) && $this->isVarExpr($node->var) && $this->hasVar((string) $this->parseIdentifier($node->var)) - && $this->detectVarType($node->var) === Type::INT + && in_array($this->detectVarType($node->var), [Type::INT, Type::FLOAT], true) ) { // std::int()/std::float() values are an explicit opt-in to native // C++ arithmetic; changing them to PHP semantics here would be as @@ -1498,6 +1526,47 @@ trait AssignOpTrait $this->assertReadonlyPropertyReferenceForbidden($expr->var, $expr, true); $this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false); + if ($this->isVarExpr($expr->var)) { + $existingLeft = $this->parseWritableIdentifier($expr->var); + if ($this->hasLocalVar($existingLeft)) { + $existingType = $this->getRawVarType($existingLeft); + if (Type::isTypedRefType($existingType) + || Type::getReferenceType($existingType) !== null + ) { + $this->fatalError( + $expr, + 'Cannot rebind fixed or typed reference variable `$' + . $this->unescapeVarName($existingLeft) . '`', + ); + } + } + } + + // A new function-local alias of fixed native storage has a stable C++ + // binding and does not need a Zend reference. References are declared + // at function scope, so only an unconditional top-level binding is + // accepted. Value expressions continue to see the referenced base type. + if ($this->isVarExpr($expr->var) && $this->isVarExpr($expr->expr)) { + $leftName = $this->parseWritableIdentifier($expr->var); + $rightName = $this->parseIdentifier($expr->expr); + if (!$this->hasVar($leftName) && $this->hasLocalVar($rightName)) { + $rightBindingType = $this->getRawVarType($rightName); + $referenceType = Type::isTypedRefType($rightBindingType) + ? $rightBindingType + : Type::getReferenceType($rightBindingType); + if ($referenceType !== null) { + if ($this->context->scopeLevel !== 1) { + $this->fatalError( + $expr, + 'A typed reference local must be bound in the top-level scope of the function', + ); + } + $this->addTypedRefLocal($leftName, $rightName, $referenceType); + return $expr->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false) ? '' : $leftName; + } + } + } + $propertyReferenceTarget = null; $nativeObjectProperty = $expr->var instanceof Expr\PropertyFetch && $this->getNativePropertyClassDef($expr->var)?->nativeObject === true; diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 772685a3..6e482f68 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -37,7 +37,7 @@ trait FunctionCallTrait $callable = $this->parseExprAsValue($expr->right); return 'typephp_call_cached(' . $callable . ', ' . $this->getFunctionCallCache() - . ', {' . $value . '})'; + . ', php::VarList{' . $value . '})'; } /** diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 9cca3ba8..d67d323e 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -1080,6 +1080,12 @@ trait PropertyAccessTrait if (!$this->hasVar($name)) { $this->errorUndefinedVariable($var); } + if ($this->isTypedRefLocal($name)) { + $this->fatalError($var, 'Cannot unset typed reference variable `$' . $this->unescapeVarName($name) . '`'); + } + if ($this->isTypedRefRoot($name)) { + $this->fatalError($var, 'Cannot unset variable `$' . $this->unescapeVarName($name) . '` while typed references point to it'); + } $type = $this->getVarType($name); if ($this->isNativeObjectVar($name)) { $this->forgetNativeObjectNonNull($name); diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index ce597f77..8c3ca8f7 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -241,6 +241,7 @@ trait TypeConversionTrait protected function convertExprFromType(string $type, string $expr): string { + $type = Type::getReferencedType($type); if ($type === Type::FLOAT) { return $this->convertFloatExpr($expr); } @@ -295,10 +296,12 @@ trait TypeConversionTrait } /** - * PHP references are untyped zval aliases. Exposing fixed C++ storage as - * one would let dynamic code replace the value with an incompatible type. - * Only php::Var (or an existing php::Ref) has storage that can safely - * participate in Zend reference semantics. + * A reference created from an ordinary local has no Zend type source. + * Exposing fixed C++ storage through such a reference would therefore let + * dynamic code replace the value with an incompatible type. Typed object + * and static properties are deliberately handled before this method: Zend + * attaches their property_info to the reference and enforces the declared + * type. PHP array elements are dynamic zval slots and use the same Ref path. */ protected function assertVariableReferenceStorage( NodeAbstract $expr, diff --git a/src/Preprocessor.php b/src/Preprocessor.php index d4ee7bbc..9900b40c 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -1087,11 +1087,52 @@ class Preprocessor extends CompilerBase // Record late-bound parameter type keywords so they can be re-resolved // to the consuming class when a trait method is flattened into a class. $argInfo->typeKeyword = $typeKeyword; - // Ordinary PHP references use php::Ref at the native ABI. Native - // object references are rejected after the complete signature has - // been parsed: a typed pointer already shares object identity, while - // PHP & would additionally expose caller-slot rebinding. - return $param->byRef ? Type::REF : $type; + if (!$param->byRef) { + return $type; + } + + // A precise scalar/string/array reference is represented by a C++ T&. + // Nullable, union, variadic and defaulted reference parameters still + // require a Zend reference because their active value/storage cannot + // be represented by one fixed native ABI type. + $referenceType = Type::getReferenceType($type); + if (!$param->variadic + && $param->default === null + && !$param->type instanceof NullableType + && !$param->type instanceof UnionType + && !$param->type instanceof IntersectionType + && $referenceType !== null + ) { + return $referenceType; + } + + // The same five value families may still need the ordinary Zend + // reference ABI when a default, nullable/union declaration, or + // variadic collection prevents a single C++ T& signature. + if ($referenceType !== null) { + return Type::REF; + } + + // Keep Native-object references on the ordinary reference ABI long + // enough for assertNativeObjectFunctionSignature() to issue the more + // precise Native-class diagnostic. They are still rejected before + // code generation. + if ($class !== '' && $this->isNativeObjectClass($class)) { + return Type::REF; + } + + // Object-like values already have identity/reference semantics. A PHP + // reference would expose replacement of the strongly typed handle and + // is deliberately unsupported by TypePHP. + if ($class !== '' || (!in_array($type, [Type::VAR, Type::REF], true) + && !$param->type instanceof NullableType + && !$param->type instanceof UnionType + && !$param->type instanceof IntersectionType + )) { + $this->fatalError($param, 'References are only supported for int, string, float, bool, array, mixed, or union types'); + } + + return Type::REF; } /** @@ -1169,7 +1210,7 @@ class Preprocessor extends CompilerBase if ($param->type === null || $param->type instanceof NullableType) { $argInfo->nullable = true; } - if (($param->byRef && $param->type !== null) + if (($param->byRef && $param->type !== null && !Type::isTypedRefType($type)) || $param->type instanceof NullableType || $param->type instanceof UnionType || $param->type instanceof IntersectionType diff --git a/src/PythonTools/Converter/PythonToTypePhpConverter.php b/src/PythonTools/Converter/PythonToTypePhpConverter.php index 829bf09a..0024a072 100644 --- a/src/PythonTools/Converter/PythonToTypePhpConverter.php +++ b/src/PythonTools/Converter/PythonToTypePhpConverter.php @@ -529,21 +529,8 @@ final class PythonToTypePhpConverter private function deleteStatement(array $node): array { $targets = []; - $walk = function (array $target) use (&$walk, &$targets, $node): void { - // del (a, b) / del [a, b] expands element by element. - if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { - foreach ($target['elts'] ?? [] as $element) { - $walk($element); - } - return; - } - if (!in_array($target['_type'] ?? '', ['Name', 'Attribute', 'Subscript'], true)) { - $this->unsupported($node, 'unsupported del target'); - } - $targets[] = $target; - }; foreach ($node['targets'] ?? [] as $target) { - $walk($target); + $this->collectDeleteTargets($target, $node, $targets); } $lines = []; foreach ($targets as $target) { @@ -552,6 +539,26 @@ final class PythonToTypePhpConverter return $lines; } + /** + * @param array $target + * @param array $node + * @param list> $targets + */ + private function collectDeleteTargets(array $target, array $node, array &$targets): void + { + // del (a, b) / del [a, b] expands element by element. + if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { + foreach ($target['elts'] ?? [] as $element) { + $this->collectDeleteTargets($element, $node, $targets); + } + return; + } + if (!in_array($target['_type'] ?? '', ['Name', 'Attribute', 'Subscript'], true)) { + $this->unsupported($node, 'unsupported del target'); + } + $targets[] = $target; + } + /** @param array $node */ private function expression(array $node): string { diff --git a/src/Resolver/ClassConstantValueTrait.php b/src/Resolver/ClassConstantValueTrait.php index 283d1bd8..70eb88fd 100644 --- a/src/Resolver/ClassConstantValueTrait.php +++ b/src/Resolver/ClassConstantValueTrait.php @@ -286,10 +286,9 @@ trait ClassConstantValueTrait ): mixed { $lateBoundScope ??= $scope; - $evaluator = null; - $evaluator = new ConstExprEvaluator(function (Node\Expr $expr) use (&$evaluator, $scope, $lateBoundScope): mixed { + $evaluator = new ConstExprEvaluator(function (Node\Expr $expr) use ($scope, $lateBoundScope): mixed { if ($expr instanceof Node\Expr\Cast) { - $value = $evaluator->evaluateDirectly($expr->expr); + $value = $this->evaluateCompileTimeExpression($expr->expr, $scope, $lateBoundScope); return match (true) { $expr instanceof Node\Expr\Cast\Int_ => (int) $value, $expr instanceof Node\Expr\Cast\Double => (float) $value, @@ -307,7 +306,7 @@ trait ClassConstantValueTrait if ($expr instanceof Node\Expr\ClassConstFetch && $expr->class instanceof Node\Name) { $name = $expr->name instanceof Node\Identifier ? $expr->name->toString() - : $evaluator->evaluateDirectly($expr->name); + : $this->evaluateCompileTimeExpression($expr->name, $scope, $lateBoundScope); if (!is_string($name)) { throw new \RuntimeException('A compile-time class constant name must evaluate to string'); } @@ -348,7 +347,7 @@ trait ClassConstantValueTrait } if ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\NullsafePropertyFetch) { - $object = $evaluator->evaluateDirectly($expr->var); + $object = $this->evaluateCompileTimeExpression($expr->var, $scope, $lateBoundScope); if ($object === null && $expr instanceof Node\Expr\NullsafePropertyFetch) { return null; } @@ -363,7 +362,7 @@ trait ClassConstantValueTrait } $property = $expr->name instanceof Node\Identifier ? $expr->name->toString() - : $evaluator->evaluateDirectly($expr->name); + : $this->evaluateCompileTimeExpression($expr->name, $scope, $lateBoundScope); if ($property === 'name') { return $object->caseName; } diff --git a/src/Testing/TestCoverageAnalyzer.php b/src/Testing/TestCoverageAnalyzer.php index 27240b94..3757611d 100644 --- a/src/Testing/TestCoverageAnalyzer.php +++ b/src/Testing/TestCoverageAnalyzer.php @@ -573,25 +573,27 @@ final class TestCoverageAnalyzer /** @param Node[] $nodes @return list */ private function findClassMethods(array $nodes): array { - $methods = []; - $this->walkValues($nodes, static function (Node $node) use (&$methods): void { + $result = new \stdClass(); + $result->values = []; + $this->walkValues($nodes, static function (Node $node) use ($result): void { if ($node instanceof Node\Stmt\ClassMethod) { - $methods[] = $node; + $result->values[] = $node; } }); - return $methods; + return $result->values; } /** @param Node[] $nodes @return list */ private function collectMethodCalls(array $nodes): array { - $calls = []; - $this->walkValues($nodes, static function (Node $node) use (&$calls): void { + $result = new \stdClass(); + $result->values = []; + $this->walkValues($nodes, static function (Node $node) use ($result): void { if ($node instanceof Expr\MethodCall && $node->name instanceof Node\Identifier) { - $calls[] = ['name' => $node->name->toString(), 'node' => $node]; + $result->values[] = ['name' => $node->name->toString(), 'node' => $node]; } }); - return $calls; + return $result->values; } private function findPhpFixtureArgument(Expr\MethodCall $call): ?string @@ -609,8 +611,9 @@ final class TestCoverageAnalyzer */ private function collectProviderRows(Node\Stmt\ClassMethod $method): array { - $rows = []; - $this->walkValues($method->stmts ?? [], static function (Node $node) use (&$rows): void { + $result = new \stdClass(); + $result->values = []; + $this->walkValues($method->stmts ?? [], static function (Node $node) use ($result): void { if (!$node instanceof Expr\Yield_ || !$node->value instanceof Expr\Array_) { return; } @@ -621,10 +624,12 @@ final class TestCoverageAnalyzer } } if ($strings !== []) { - $rows[] = $strings; + $result->values[] = $strings; } }); + $rows = $result->values; + if ($rows !== []) { return $rows; } @@ -686,11 +691,14 @@ final class TestCoverageAnalyzer return $fixtures; } if ($value instanceof Node) { - $this->walkValues($value, static function (Node $node) use (&$fixtures): void { + $result = new \stdClass(); + $result->values = []; + $this->walkValues($value, static function (Node $node) use ($result): void { if ($node instanceof Node\Scalar\String_ && str_ends_with($node->value, '.php')) { - $fixtures[] = $node->value; + $result->values[] = $node->value; } }); + $fixtures = $result->values; } return array_values(array_unique($fixtures)); } @@ -808,8 +816,10 @@ final class TestCoverageAnalyzer /** @param list $ancestors @param array $features */ private function detectSemanticNodeFeatures(Node $node, array $ancestors, array &$features): void { - $mark = static function (string $id) use (&$features): void { - $features['semantic:' . $id] = true; + $result = new \stdClass(); + $result->features = $features; + $mark = static function (string $id) use ($result): void { + $result->features['semantic:' . $id] = true; }; if ($node instanceof Node\Name\Relative) { @@ -916,13 +926,16 @@ final class TestCoverageAnalyzer } } } + $features = $result->features; } /** @param array $features */ private function detectSourceFeatures(string $code, array &$features): void { - $mark = static function (string $id) use (&$features): void { - $features['semantic:' . $id] = true; + $result = new \stdClass(); + $result->features = $features; + $mark = static function (string $id) use ($result): void { + $result->features['semantic:' . $id] = true; }; if (preg_match('/\b(?:if|elseif|for|foreach|while|switch)\s*\([^;{}]*\)\s*:/s', $code) || preg_match('/\bend(?:if|for|foreach|while|switch)\s*;/i', $code) @@ -941,6 +954,7 @@ final class TestCoverageAnalyzer if (preg_match('/\b(?:exit|die)\s*\(\s*message\s*:/i', $code)) { $mark('exit_named_argument'); } + $features = $result->features; } private function unionContainsIntersection(Node\UnionType $type): bool diff --git a/src/Translator.php b/src/Translator.php index e05e63e4..16547960 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4594,9 +4594,14 @@ CODE; // separate pass so one trait method can call another method declared // later in the same or a nested trait. foreach ($composedTraitMethods as [$stmt, $origin]) { - $this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void { - $this->parseClassMethod($stmt, $methodCodes); + $traitMethodCodes = $this->withTraitNameContext($origin, function () use ($stmt): array { + $codes = []; + $this->parseClassMethod($stmt, $codes); + return $codes; }); + foreach ($traitMethodCodes as $methodName => $methodCode) { + $methodCodes[$methodName] = $methodCode; + } } if (!$class instanceof Node\Stmt\Trait_) { $this->validateOverrideAttributes($class); @@ -4751,6 +4756,7 @@ CODE; } $callParams = ''; + $refWrapVars = []; foreach ($functionDef->argInfoList as $k => $argInfo) { $var = 'arg_' . $argInfo->name; if ($argInfo->variadic) { @@ -4791,7 +4797,18 @@ CODE; } $cppType = $this->getDefaultArgumentType($argInfo); $declaredClass = $argInfo->declaredClass ?: $argInfo->class; - if ($this->isStrictScalarType($argInfo->type)) { + $expr = ''; + if (Type::isTypedRefType($argInfo->type)) { + $referencedType = Type::getReferencedType($argInfo->type); + $refVar = 'ref_' . $var; + $wrapVar = 'wrap_' . $var; + $refWrapVars[] = $wrapVar; + $cppCode .= $this->getIndent() . Type::REF . ' ' . $refVar . ' = ' . $argExpr . ';' . PHP_EOL; + $cppCode .= $this->getIndent() . 'php::RefWrap<' . $referencedType . '> ' + . $wrapVar . '(' . $refVar . ');' . PHP_EOL; + $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' + . $wrapVar . '.typed();' . PHP_EOL; + } elseif ($this->isStrictScalarType($argInfo->type)) { $rawVar = 'raw_' . $var; $cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = ' . $argExpr . ';' . PHP_EOL; $cppCode .= $this->genStrictScalarParamCheck( @@ -4806,7 +4823,9 @@ CODE; } else { $expr = $this->convertExprFromType($argInfo->type, $argExpr); } - $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL; + if (!Type::isTypedRefType($argInfo->type)) { + $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL; + } } $callParam = $var; if ($this->canConsumeForwardedArgument($argInfo)) { @@ -4832,12 +4851,18 @@ CODE; if ($functionDef->returnType !== Type::VOID) { $cppCode .= $this->getIndent() . 'auto retval = ' . $fn . '(' . $callParams . ');' . PHP_EOL; + foreach ($refWrapVars as $refWrapVar) { + $cppCode .= $this->getIndent() . $refWrapVar . '.commit();' . PHP_EOL; + } $cppCode .= $this->getIndent() . 'php::move(retval, return_value);' . PHP_EOL; if (!$functionDef->returnsByRef) { $cppCode .= $this->getIndent() . 'php::deref(return_value);' . PHP_EOL; } } else { $cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL; + foreach ($refWrapVars as $refWrapVar) { + $cppCode .= $this->getIndent() . $refWrapVar . '.commit();' . PHP_EOL; + } } $this->indentLevel--; $cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL; @@ -5833,7 +5858,9 @@ CODE; } $declaredClass = $arg->declaredClass ?: $arg->class; - return match ($arg->type) { + // Typed-ref is a native ABI detail. Signature compatibility is based + // on the PHP value type plus the independently checked byRef flag. + return match (Type::getReferencedType($arg->type)) { Type::INT => [['kind' => 'isInt']], Type::FLOAT => [['kind' => 'isFloat']], Type::BOOL => [['kind' => 'isBool']], diff --git a/src/Type.php b/src/Type.php index 9ef837a9..6b706e9c 100644 --- a/src/Type.php +++ b/src/Type.php @@ -24,5 +24,50 @@ final class Type public const string ARGS = 'php::Args'; public const string STR = 'php::Str'; public const string REF = 'php::Ref'; + public const string INT_REF = 'php::Int &'; + public const string STR_REF = 'php::Str &'; + public const string FLOAT_REF = 'php::Float &'; + public const string BOOL_REF = 'php::Bool &'; + public const string ARRAY_REF = 'php::Array &'; public const string VOID = 'void'; + + public static function isTypedRefType(string $type): bool + { + return in_array($type, [ + self::INT_REF, + self::STR_REF, + self::FLOAT_REF, + self::BOOL_REF, + self::ARRAY_REF, + ], true); + } + + public static function isAnyRefType(string $type): bool + { + return $type === self::REF || self::isTypedRefType($type); + } + + public static function getReferenceType(string $type): ?string + { + return match ($type) { + self::INT => self::INT_REF, + self::STR => self::STR_REF, + self::FLOAT => self::FLOAT_REF, + self::BOOL => self::BOOL_REF, + self::ARRAY => self::ARRAY_REF, + default => null, + }; + } + + public static function getReferencedType(string $type): string + { + return match ($type) { + self::INT_REF => self::INT, + self::STR_REF => self::STR, + self::FLOAT_REF => self::FLOAT, + self::BOOL_REF => self::BOOL, + self::ARRAY_REF => self::ARRAY, + default => $type, + }; + } } diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 06f5b95e..3d4ce951 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -257,6 +257,51 @@ trait NativeTypeCompatibilityTrait $this->addLocalVar($var, Type::VAR); } } + + if (Type::isTypedRefType($argInfo->type)) { + $expectedType = Type::getReferencedType($argInfo->type); + $actualType = Type::getReferencedType($this->detectTypeOfExpr($arg->value)); + if ($this->isVarExpr($arg->value)) { + $var = $this->parseIdentifier($arg->value); + $rawType = $this->getRawVarType($var); + if ($actualType === $expectedType + && ($rawType === $expectedType || $rawType === $argInfo->type) + ) { + return $var; + } + } + + if ($actualType !== $expectedType + && !in_array($actualType, [Type::VAR, Type::REF], true) + ) { + $this->fatalError( + $arg, + 'Cannot pass value of type ' . $actualType + . ' to reference parameter of type ' . $argInfo->type, + ); + } + + $reference = $this->addTmpVar(Type::REF); + $wrapper = $this->genTmpVarName(); + $this->context->beforeStmtLines[] = $reference . ' = ' . $this->convertToRef($arg->value) . ';'; + $this->context->beforeStmtLines[] = 'php::RefWrap<' . $expectedType . '> ' + . $wrapper . '(' . $reference . ');'; + $this->context->afterStmtLines[] = $wrapper . '.commit();'; + return $wrapper . '.typed()'; + } + + // A mixed/union/defaulted reference parameter keeps the php::Ref + // ABI. When its caller is one of the five fixed native locals, + // bridge that storage for exactly this call and validate the + // write-back afterwards instead of weakening the local to Var. + if ($this->isVarExpr($arg->value)) { + $var = $this->parseIdentifier($arg->value); + $rawType = $this->getRawVarType($var); + $valueType = Type::getReferencedType($rawType); + if (Type::getReferenceType($valueType) !== null) { + return $this->getDynamicTypedRefBridge($var, $valueType) . '.ref()'; + } + } return $this->convertToRef($arg->value); } diff --git a/src/gen_stub.php b/src/gen_stub.php index ed2cda44..1c4a5f50 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2642,19 +2642,23 @@ class EvaluatedValue $evaluationState = new \stdClass(); $evaluationState->isUnknownConstValue = false; - $evaluator = null; - $evaluator = new ConstExprEvaluator( + // The holder permits recursive evaluator access without taking a + // reference to an object-typed local. TypePHP intentionally rejects + // object references because objects already have identity. + $evaluator = new \stdClass(); + $evaluator->instance = null; + $evaluator->instance = new ConstExprEvaluator( static function (Expr $expr) use ( $allConstInfos, $evaluationState, - &$evaluator, + $evaluator, ) { // php-parser's ConstExprEvaluator predates PHP 8.5 constant // expression casts. Keep the compatibility logic in TypePHP: // validation has already rejected void and disallowed object // casts before declaration values reach gen_stub.php. if ($expr instanceof Expr\Cast) { - $value = $evaluator->evaluateDirectly($expr->expr); + $value = $evaluator->instance->evaluateDirectly($expr->expr); return match (true) { $expr instanceof Expr\Cast\Int_ => (int) $value, $expr instanceof Expr\Cast\Double => (float) $value, @@ -2737,7 +2741,7 @@ class EvaluatedValue } ); - $result = $evaluator->evaluateDirectly($expr); + $result = $evaluator->instance->evaluateDirectly($expr); $enumCaseRef = null; if ($result instanceof \TypePhp\Entity\EnumCaseRef) { @@ -6049,13 +6053,14 @@ function generateArgInfoCode( $code .= "\n"; } - $generatedFuncInfos = []; + $generatedFuncInfos = new \stdClass(); + $generatedFuncInfos->values = []; $argInfoCode = generateCodeWithConditions( $fileInfo->getAllFuncInfos(), "\n", - static function (FuncInfo $funcInfo) use (&$generatedFuncInfos, $fileInfo) { + static function (FuncInfo $funcInfo) use ($generatedFuncInfos, $fileInfo) { /* If there already is an equivalent arginfo structure, only emit a #define */ - if ($generatedFuncInfo = $funcInfo->findEquivalent($generatedFuncInfos)) { + if ($generatedFuncInfo = $funcInfo->findEquivalent($generatedFuncInfos->values)) { $code = sprintf( "#define %s %s\n", $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() @@ -6064,7 +6069,7 @@ function generateArgInfoCode( $code = $funcInfo->toArgInfoCode($fileInfo->getMinimumPhpVersionIdCompatibility()); } - $generatedFuncInfos[] = $funcInfo; + $generatedFuncInfos->values[] = $funcInfo; return $code; } ); @@ -6085,16 +6090,17 @@ function generateArgInfoCode( $code .= "$framelessFunctionCode\n"; } - $generatedFunctionDeclarations = []; + $generatedFunctionDeclarations = new \stdClass(); + $generatedFunctionDeclarations->keys = []; $code .= generateCodeWithConditions( $fileInfo->getAllFuncInfos(), "", - static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarations) { + static function (FuncInfo $funcInfo) use ($fileInfo, $generatedFunctionDeclarations) { $key = $funcInfo->getDeclarationKey(); - if (isset($generatedFunctionDeclarations[$key])) { + if (isset($generatedFunctionDeclarations->keys[$key])) { return null; } - $generatedFunctionDeclarations[$key] = true; + $generatedFunctionDeclarations->keys[$key] = true; return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); } ); @@ -6184,10 +6190,12 @@ function generateFunctionEntries(?Name $className, array $funcInfos, ?string $co * the name of a zend_string already created with that content */ function generateFunctionAttributeInitialization(iterable $funcInfos, array $allConstInfos, ?int $phpVersionIdMinimumCompatibility, ?string $parentCond = null, array &$declaredStrings = []): string { - return generateCodeWithConditions( + $declaredStringState = new \stdClass(); + $declaredStringState->values = $declaredStrings; + $code = generateCodeWithConditions( $funcInfos, "", - static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, &$declaredStrings) { + static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, $declaredStringState) { $code = null; if ($funcInfo->name instanceof MethodName) { @@ -6200,10 +6208,10 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all // conditionally available; string reuse is only among declarations // that are always there if ($funcInfo->cond) { - $empty = []; - $useDeclared = &$empty; + $useDeclared = new \stdClass(); + $useDeclared->values = []; } else { - $useDeclared = &$declaredStrings; + $useDeclared = $declaredStringState; } foreach ($funcInfo->attributes as $key => $attribute) { @@ -6213,7 +6221,7 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all "func_" . $funcInfo->name->getNameForAttributes() . "_$key", $allConstInfos, $phpVersionIdMinimumCompatibility, - \std::ref($useDeclared) + \std::ref($useDeclared->values) ); } @@ -6225,7 +6233,7 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all "func_{$funcInfo->name->getNameForAttributes()}_arg{$index}_$key", $allConstInfos, $phpVersionIdMinimumCompatibility, - \std::ref($useDeclared) + \std::ref($useDeclared->values) ); } } @@ -6234,6 +6242,13 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all }, $parentCond ); + // Keep the property read as a separate PHP value assignment. Calls above + // turn `values` into a zend_reference; assigning that reference expression + // directly to the by-reference parameter would rebind the local Reference + // wrapper instead of writing the array back to its caller. + $updatedDeclaredStrings = $declaredStringState->values; + $declaredStrings = $updatedDeclaredStrings; + return $code; } /** @@ -6253,10 +6268,12 @@ function generateGlobalConstantAttributeInitialization( if ($phpVersionIdMinimumCompatibility !== null && $phpVersionIdMinimumCompatibility < PHP_85_VERSION_ID) { $isConditional = true; } + $declaredStringState = new \stdClass(); + $declaredStringState->values = $declaredStrings; $code = generateCodeWithConditions( $constInfos, "", - static function (ConstInfo $constInfo) use ($allConstInfos, $isConditional, &$declaredStrings) { + static function (ConstInfo $constInfo) use ($allConstInfos, $isConditional, $declaredStringState) { $code = ""; if ($constInfo->attributes === []) { @@ -6266,10 +6283,10 @@ function generateGlobalConstantAttributeInitialization( // conditionally available; string reuse is only among declarations // that are always there if ($constInfo->cond) { - $empty = []; - $useDeclared = &$empty; + $useDeclared = new \stdClass(); + $useDeclared->values = []; } else { - $useDeclared = &$declaredStrings; + $useDeclared = $declaredStringState; } $constName = str_replace('\\', '\\\\', $constInfo->name->__toString()); $constVarName = 'const_' . $constName; @@ -6281,7 +6298,7 @@ function generateGlobalConstantAttributeInitialization( $constVarName . "_$key", $allConstInfos, PHP_85_VERSION_ID, - \std::ref($useDeclared) + \std::ref($useDeclared->values) ); } @@ -6289,6 +6306,8 @@ function generateGlobalConstantAttributeInitialization( }, $parentCond ); + $updatedDeclaredStrings = $declaredStringState->values; + $declaredStrings = $updatedDeclaredStrings; if ($code && $isConditional) { return "\n#if (PHP_VERSION_ID >= " . PHP_85_VERSION_ID . ")\n" . $code . "#endif\n"; } @@ -6308,20 +6327,22 @@ function generateConstantAttributeInitialization( ?string $parentCond = null, array &$declaredStrings = [] ): string { - return generateCodeWithConditions( + $declaredStringState = new \stdClass(); + $declaredStringState->values = $declaredStrings; + $code = generateCodeWithConditions( $constInfos, "", - static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, &$declaredStrings) { + static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, $declaredStringState) { $code = null; // Make sure we don't try and use strings that might only be // conditionally available; string reuse is only among declarations // that are always there if ($constInfo->cond) { - $empty = []; - $useDeclared = &$empty; + $useDeclared = new \stdClass(); + $useDeclared->values = []; } else { - $useDeclared = &$declaredStrings; + $useDeclared = $declaredStringState; } foreach ($constInfo->attributes as $key => $attribute) { $code .= $attribute->generateCode( @@ -6329,7 +6350,7 @@ function generateConstantAttributeInitialization( "const_" . $constInfo->name->getDeclarationName() . "_$key", $allConstInfos, $phpVersionIdMinimumCompatibility, - \std::ref($useDeclared) + \std::ref($useDeclared->values) ); } @@ -6337,6 +6358,9 @@ function generateConstantAttributeInitialization( }, $parentCond ); + $updatedDeclaredStrings = $declaredStringState->values; + $declaredStrings = $updatedDeclaredStrings; + return $code; } /** @@ -7161,34 +7185,34 @@ function generateStubFile(string $stubFile, string $objectFile, bool $forceRegen } } - array_map( - function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { - if ($aliasArg === null) { - assert($aliasedArg !== null); - $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; - return null; - } + $argCount = max(count($aliasArgs), count($aliasedArgs)); + for ($argIndex = 0; $argIndex < $argCount; $argIndex++) { + $aliasArg = $aliasArgs[$argIndex] ?? null; + $aliasedArg = $aliasedArgs[$argIndex] ?? null; + if ($aliasArg === null) { + assert($aliasedArg !== null); + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; + continue; + } - if ($aliasedArg === null) { - $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; - return null; - } + if ($aliasedArg === null) { + $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; + continue; + } - if ($aliasArg->name !== $aliasedArg->name) { - $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; - return null; - } + if ($aliasArg->name !== $aliasedArg->name) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; + continue; + } - if ($aliasArg->type != $aliasedArg->type) { - $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; - } + if ($aliasArg->type != $aliasedArg->type) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; + } - if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { - $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; - } - }, - $aliasArgs, $aliasedArgs - ); + if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { + $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; + } + } $aliasedReturn = $aliasedFunc->return; $aliasReturn = $aliasFunc->return; diff --git a/tests/compiler/array_method/method.phpt b/tests/compiler/array_method/method.phpt index b3bb1917..3a1f6903 100644 --- a/tests/compiler/array_method/method.phpt +++ b/tests/compiler/array_method/method.phpt @@ -140,33 +140,33 @@ function main() Assert::true($array->isEmpty()); $fruits1 = array("lemon", "orange", "banana", "apple"); - $fruits2 = std::any(array("lemon", "orange", "banana", "apple")); + $fruits2 = array("lemon", "orange", "banana", "apple"); sort($fruits2); Assert::eq($fruits1->sort(), $fruits2); Assert::eq($fruits1, $fruits2); - $stack = std::any(array("orange", "banana", "apple", "raspberry")); + $stack = array("orange", "banana", "apple", "raspberry"); Assert::eq($stack->pop(), 'raspberry'); Assert::eq(array_pop($stack), 'apple'); - $mutableArray = std::any(array("red","green")); + $mutableArray = array("red","green"); $mutableArray->push("blue"); Assert::eq($mutableArray, ["red","green", "blue"]); array_push($mutableArray, "yellow"); Assert::eq($mutableArray, ["red","green", "blue", "yellow"]); - $stack = std::any(array("orange", "banana", "apple", "raspberry")); + $stack = array("orange", "banana", "apple", "raspberry"); Assert::eq($stack->shift(), 'orange'); Assert::eq(array_shift($stack), 'banana'); - $queue = std::any(["orange", "banana"]); + $queue = ["orange", "banana"]; $queue->unshift("orange"); Assert::eq($queue, ["orange", "orange", "banana"]); array_unshift($queue, "orange"); Assert::eq($queue, ["orange", "orange", "orange", "banana"]); $array1 = array("red", "green", "blue", "yellow"); - $spliceArray = std::any(array("red", "green", "blue", "yellow")); + $spliceArray = array("red", "green", "blue", "yellow"); Assert::eq($array1->splice(2), array_splice($spliceArray, 2)); Assert::eq($array1, $spliceArray); diff --git a/tests/compiler/attribute/immutable.phpt b/tests/compiler/attribute/immutable.phpt index e20f564d..ec65173a 100644 --- a/tests/compiler/attribute/immutable.phpt +++ b/tests/compiler/attribute/immutable.phpt @@ -14,11 +14,6 @@ function sumImmutable(#[Immutable] array $values): int return count($values) + $values->count() + $values[0] + $values[1]; } -function inspectImmutableReference(#[Immutable] ImmutableUser &$user): string -{ - return $user->name(); -} - trait ImmutableNameTrait { #[Immutable] @@ -118,8 +113,6 @@ function main(): void echo $user->traitName(), PHP_EOL; echo sumImmutable([2, 3]), PHP_EOL; echo cloneImmutable($user), PHP_EOL; - $referenceUser = std::any($user); - echo inspectImmutableReference($referenceUser), PHP_EOL; echo deliberatelyEscapeImmutableCheck($user), PHP_EOL; echo (new ImmutableHookedValue())->read(), PHP_EOL; echo closureImmutableParameter($user), PHP_EOL; @@ -131,7 +124,6 @@ Rango:Rango Rango 9 Clone -Rango Dynamic HOOK Dynamic diff --git a/tests/compiler/basic/pow-int-overflow.phpt b/tests/compiler/basic/pow-int-overflow.phpt index 2d8a4cfc..41aeacd1 100644 --- a/tests/compiler/basic/pow-int-overflow.phpt +++ b/tests/compiler/basic/pow-int-overflow.phpt @@ -2,6 +2,8 @@ pow int overflow --FILE-- --EXPECT-- -1.2089258196146292E+24 \ No newline at end of file +1.2089258196146292E+24 diff --git a/tests/compiler/basic/unset.phpt b/tests/compiler/basic/unset.phpt index 4aa330ec..e5d59d02 100644 --- a/tests/compiler/basic/unset.phpt +++ b/tests/compiler/basic/unset.phpt @@ -5,7 +5,7 @@ unset on arrays and variables function main() { // Unset variable - $a = 10; + $a = std::any(10); unset($a); var_dump(isset($a)); diff --git a/tests/compiler/coalesce/unset-ref-coalesce.phpt b/tests/compiler/coalesce/unset-ref-coalesce.phpt index c1d747c4..ad6ef486 100644 --- a/tests/compiler/coalesce/unset-ref-coalesce.phpt +++ b/tests/compiler/coalesce/unset-ref-coalesce.phpt @@ -55,7 +55,7 @@ function main() var_dump($k); // Test 7: normal variable unset + ?? - $m = 50; + $m = std::any(50); unset($m); var_dump($m ?? 777); diff --git a/tests/compiler/const/php-int-max-namespace-shadow.phpt b/tests/compiler/const/php-int-max-namespace-shadow.phpt index d25063de..ad3f9438 100644 --- a/tests/compiler/const/php-int-max-namespace-shadow.phpt +++ b/tests/compiler/const/php-int-max-namespace-shadow.phpt @@ -4,6 +4,8 @@ Namespaced PHP_INT_MAX shadows the global constant in unqualified fetches diff --git a/tests/compiler/optimizations/int-calculation.phpt b/tests/compiler/optimizations/int-calculation.phpt index 86854ccc..c74097bb 100644 --- a/tests/compiler/optimizations/int-calculation.phpt +++ b/tests/compiler/optimizations/int-calculation.phpt @@ -2,6 +2,8 @@ SSA: int --FILE-- --EXPECTF-- 12 -9.2233720368547758E+18 \ No newline at end of file +9.2233720368547758E+18 diff --git a/tests/compiler/ref/010.phpt b/tests/compiler/ref/010.phpt new file mode 100644 index 00000000..dbf3d344 --- /dev/null +++ b/tests/compiler/ref/010.phpt @@ -0,0 +1,22 @@ +--TEST-- +class const 001 +--FILE-- + +--EXPECT-- +int(390) diff --git a/tests/compiler/ref/011.phpt b/tests/compiler/ref/011.phpt new file mode 100644 index 00000000..b079c4c4 --- /dev/null +++ b/tests/compiler/ref/011.phpt @@ -0,0 +1,15 @@ +--TEST-- +class const 001 +--FILE-- + +--EXPECT-- +int(130) diff --git a/tests/compiler/ref/nested-reference-return.phpt b/tests/compiler/ref/nested-reference-return.phpt index edcb44a8..788a6e94 100644 --- a/tests/compiler/ref/nested-reference-return.phpt +++ b/tests/compiler/ref/nested-reference-return.phpt @@ -25,6 +25,11 @@ function &object_property_ref(NestedReferenceBox $box): mixed function main(): void { + $fixedValues = ['item' => 'fixed-before']; + $fixedItem =& array_element_ref($fixedValues); + $fixedItem = 'fixed-after'; + var_dump($fixedValues['item']); + $values = std::any([ 'item' => 'before', 'outer' => ['inner' => 'nested-before'], @@ -45,6 +50,7 @@ function main(): void } ?> --EXPECT-- +string(11) "fixed-after" string(5) "after" string(12) "nested-after" string(12) "object-after" diff --git a/tests/compiler/ref/property-reference-rebind.phpt b/tests/compiler/ref/property-reference-rebind.phpt index 27934301..5270ea55 100644 --- a/tests/compiler/ref/property-reference-rebind.phpt +++ b/tests/compiler/ref/property-reference-rebind.phpt @@ -12,7 +12,7 @@ final class PrivatePropertyReferenceHolder { private ?array $value = null; - public function bind(array &$source): void + public function bind(mixed &$source): void { $this->value = &$source; } diff --git a/tests/compiler/ref/ref-call-arg.phpt b/tests/compiler/ref/ref-call-arg.phpt index 3452f0f5..2da1b205 100644 --- a/tests/compiler/ref/ref-call-arg.phpt +++ b/tests/compiler/ref/ref-call-arg.phpt @@ -1,5 +1,11 @@ --TEST-- ref call arg +--SKIPIF-- + --FILE-- close(); if ($zip->open($path) === TRUE) { + $opsys = std::any(null); + $attr = std::any(null); for ($idx = 0; $s = $zip->statIndex($idx); $idx++) { - $rs = $zip->getExternalAttributesIndex($idx, $opsys, $attr); + $rs = $zip->getExternalAttributesIndex($idx, std::ref($opsys), std::ref($attr)); var_dump($rs, $idx, $opsys, $attr); } $zip->close(); diff --git a/tests/compiler/ref/ref-object.phpt b/tests/compiler/ref/ref-object.phpt index b576e36c..fb54b84d 100644 --- a/tests/compiler/ref/ref-object.phpt +++ b/tests/compiler/ref/ref-object.phpt @@ -1,5 +1,5 @@ ---TEST-- -Reference parameter with object type - property access and method call +--TEST-- +Mixed reference parameters can replace objects while preserving object operations --FILE-- value); var_dump($test->abc()); diff --git a/tests/compiler/ref/typed-native-call-forms.phpt b/tests/compiler/ref/typed-native-call-forms.phpt new file mode 100644 index 00000000..4e843ba7 --- /dev/null +++ b/tests/compiler/ref/typed-native-call-forms.phpt @@ -0,0 +1,64 @@ +--TEST-- +Typed-reference ABI supports functions, methods, constructors, interfaces, and named arguments +--FILE-- +mutate($value, 4); +} + +function main(): void +{ + $number = 1; + new TypedReferenceTarget(value: $number); + + $text = 'value'; + $target = new TypedReferenceTarget($number); + $target->mutate(amount: 3, value: $text); + mutateThroughInterface($target, $text); + + $enabled = false; + TypedReferenceTarget::toggle(value: $enabled); + + $float = 1.5; + widenFloat(value: $float); + + var_dump($number, $text, $enabled, $float); +} + +?> +--EXPECT-- +int(21) +string(9) "value:3:4" +bool(true) +float(7) diff --git a/tests/compiler/ref/typed-native-local-reference.phpt b/tests/compiler/ref/typed-native-local-reference.phpt new file mode 100644 index 00000000..cb568fe6 --- /dev/null +++ b/tests/compiler/ref/typed-native-local-reference.phpt @@ -0,0 +1,103 @@ +--TEST-- +Native typed local references preserve alias and value-copy semantics +--FILE-- + +--EXPECT-- +int(103) +int(103) +int(200) +int(103) +string(13) "value:changed" +string(13) "value:changed" +string(5) "value" +float(4) +float(4) +bool(true) +bool(true) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(4) +} diff --git a/tests/compiler/ref/typed-property-and-array-reference.phpt b/tests/compiler/ref/typed-property-and-array-reference.phpt new file mode 100644 index 00000000..cb12623b --- /dev/null +++ b/tests/compiler/ref/typed-property-and-array-reference.phpt @@ -0,0 +1,106 @@ +--TEST-- +Typed properties retain Zend reference type sources while PHP array elements remain dynamic +--FILE-- +value); + appendSuffix(TypedReferenceHolder::$staticValue); + var_dump($holder->value, TypedReferenceHolder::$staticValue); + + try { + replaceWithArray($holder->value); + echo "missing instance TypeError\n"; + } catch (TypeError $error) { + echo "instance TypeError\n"; + } + + try { + replaceWithArray(TypedReferenceHolder::$staticValue); + echo "missing static TypeError\n"; + } catch (TypeError $error) { + echo "static TypeError\n"; + } + + $array = []; + $array['dynamic'] = 'array'; + appendSuffix($array['dynamic']); + replaceWithArray($array['dynamic']); + var_dump($array['dynamic']); + + $array['typed'] =& $holder->value; + appendSuffix($array['typed']); + try { + replaceWithArray($array['typed']); + echo "missing propagated TypeError\n"; + } catch (TypeError $error) { + echo "propagated TypeError\n"; + } + + var_dump($holder->value, $array['typed']); + + $throwHolder = new TypedReferenceHolder(); + try { + appendThenThrow($throwHolder->value); + } catch (RuntimeException $error) { + echo "property throw\n"; + } + $throwArray = ['value' => 'array']; + try { + appendThenThrow($throwArray['value']); + } catch (RuntimeException $error) { + echo "array throw\n"; + } + var_dump($throwHolder->value, $throwArray['value']); + + $callback = 'appendSuffix'; + $callback(std::ref($holder->value)); + $array['dynamic'] = 'dynamic'; + $callback(std::ref($array['dynamic'])); + var_dump($holder->value, $array['dynamic']); +} + +?> +--EXPECT-- +string(16) "instance:changed" +string(14) "static:changed" +instance TypeError +static TypeError +array(1) { + [0]=> + string(7) "invalid" +} +propagated TypeError +string(24) "instance:changed:changed" +string(24) "instance:changed:changed" +property throw +array throw +string(14) "instance:throw" +string(11) "array:throw" +string(32) "instance:changed:changed:changed" +string(15) "dynamic:changed" diff --git a/tests/compiler/ref/typed-reference-dynamic-bridge.phpt b/tests/compiler/ref/typed-reference-dynamic-bridge.phpt new file mode 100644 index 00000000..1a316582 --- /dev/null +++ b/tests/compiler/ref/typed-reference-dynamic-bridge.phpt @@ -0,0 +1,177 @@ +--TEST-- +Typed native references bridge safely across mixed and dynamic Zend calls +--FILE-- + typed-ref bridge nested in another call must commit before the + // following outer argument observes the same source variable. + $dynamicInt = std::any(20); + var_dump(incrementTyped($dynamicInt), $dynamicInt); + + $increment = function (&$value): int { + $value++; + return $value; + }; + // The inner bridge must commit before the second outer argument is read. + observePair($increment(std::ref($int)), $int); + + $thrower = function (&$value): void { + $value = 99; + throw new RuntimeException('stop'); + }; + try { + $thrower(std::ref($int)); + } catch (RuntimeException $error) { + echo "caught\n"; + } + var_dump($int); + + // Both arguments have one canonical root and must share one temporary + // zend_reference within this call. + $alias =& $int; + $mutateBoth = function (&$left, &$right): void { + $left = 10; + $right++; + }; + $mutateBoth(std::ref($int), std::ref($alias)); + var_dump($int, $alias); + + // A dynamic callee may not retain a temporary reference beyond the call. + $box = new stdClass(); + $escape = function (&$value) use ($box): void { + $box->value =& $value; + }; + try { + $escape(std::ref($int)); + } catch (Error $error) { + echo "escape error\n"; + } + var_dump($int, $box->value); + + // Compiler-owned unpack containers must release their temporary + // reference before the bridge performs its escape check and write-back. + $add = function (&$value, int $amount): void { + $value += $amount; + }; + $tail = [2]; + $add(std::ref($int), ...$tail); + var_dump($int); + + $string = 'a'; + replaceMixed($string, 'b'); + $float = 1.5; + replaceMixed($float, 2.5); + $bool = false; + replaceMixed($bool, true); + $array = [1]; + replaceMixed($array, [2]); + var_dump($string, $float, $bool, $array); + + // RefWrap writeback is part of evaluating the call expression. A later + // array item must observe it, for both compact and statement-built array + // literal lowering paths. + $array = [1]; + $orderedList = [appendMixed($array, 2), $array]; + $orderedMap = ['count' => appendMixed($array, 3), 0 => $array]; + var_dump($orderedList, $orderedMap); +} + +?> +--EXPECT-- +int(2) +type error +int(2) +assignment type error +int(4) +int(4) +int(21) +int(21) +int(5) +int(5) +caught +int(99) +int(11) +int(11) +escape error +int(11) +int(11) +int(13) +string(1) "b" +float(2.5) +bool(true) +array(1) { + [0]=> + int(2) +} +array(2) { + [0]=> + int(2) + [1]=> + array(2) { + [0]=> + int(1) + [1]=> + int(2) + } +} +array(2) { + ["count"]=> + int(3) + [0]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } +} diff --git a/tests/compiler/static/static-property-function-local-slot.phpt b/tests/compiler/static/static-property-function-local-slot.phpt index dfd73bc2..734aa98c 100644 --- a/tests/compiler/static/static-property-function-local-slot.phpt +++ b/tests/compiler/static/static-property-function-local-slot.phpt @@ -61,7 +61,7 @@ array(3) { [1]=> NULL [2]=> - &string(9) "reference" + string(9) "reference" } string(9) "reference" string(11) "child-write" diff --git a/tests/compiler/type_hits/native-type.phpt b/tests/compiler/type_hits/native-type.phpt index 4bf6ad02..1b0710e2 100644 --- a/tests/compiler/type_hits/native-type.phpt +++ b/tests/compiler/type_hits/native-type.phpt @@ -37,6 +37,6 @@ float(100) bool(true) int(99) float(2026) -float(2.5) int(2) -float(10) \ No newline at end of file +int(2) +float(10) diff --git a/tests/compiler/variadic/by-reference-closure-callback.phpt b/tests/compiler/variadic/by-reference-closure-callback.phpt index b8c2f9ca..f62dd86d 100644 --- a/tests/compiler/variadic/by-reference-closure-callback.phpt +++ b/tests/compiler/variadic/by-reference-closure-callback.phpt @@ -6,14 +6,19 @@ Reference Closure parameters work at Zend callback boundaries used by Symfony mb function convert_values(string $suffix, &...$vars): bool { $ok = std::any(true); - foreach ($vars as &$var) { - array_walk_recursive($var, static function (&$value, $key) use (&$ok, $suffix): void { - if (!is_string($value)) { - $ok = false; - return; - } - $value .= $suffix . ':' . $key; - }); + $convert = static function (&$value, $key) use (&$ok, $suffix): void { + if (!is_string($value)) { + $ok = false; + return; + } + $value .= $suffix . ':' . $key; + }; + foreach ($vars as $index => &$var) { + if (is_array($var)) { + array_walk_recursive($var, $convert); + } else { + $convert(std::ref($var), $index); + } } unset($var); return $ok; diff --git a/tests/compiler/variadic/by-reference-types.phpt b/tests/compiler/variadic/by-reference-types.phpt index 87a994db..6dbb1bff 100644 --- a/tests/compiler/variadic/by-reference-types.phpt +++ b/tests/compiler/variadic/by-reference-types.phpt @@ -26,7 +26,7 @@ function normalize(int|string &...$values): void unset($value); } -function bump_objects(Counter &...$values): void +function bump_objects(mixed &...$values): void { foreach ($values as $value) { $value->value++;