Refactoring: support strongly typed references

master
韩天峰 1 month ago
parent 50a8ce1570
commit 6215ecd06d
  1. 38
      CHANGELOG.md
  2. 30
      docs/en/INCOMPATIBLE_PHP_FEATURES.md
  3. 21
      docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md
  4. 168
      docs/zh-cn/TYPED_NATIVE_REFERENCE_DESIGN.md
  5. 9
      phpunit/code/typed-reference-argument-type-error.php
  6. 8
      phpunit/code/typed-reference-array-escape.php
  7. 8
      phpunit/code/typed-reference-assignment-type-error.php
  8. 9
      phpunit/code/typed-reference-conditional-bind.php
  9. 8
      phpunit/code/typed-reference-object-closure-param.php
  10. 5
      phpunit/code/typed-reference-object-param.php
  11. 6
      phpunit/code/typed-reference-parameter-return.php
  12. 13
      phpunit/code/typed-reference-property-escape.php
  13. 9
      phpunit/code/typed-reference-rebind.php
  14. 9
      phpunit/code/typed-reference-root-rebind.php
  15. 8
      phpunit/code/typed-reference-unset-alias.php
  16. 8
      phpunit/code/typed-reference-unset-root.php
  17. 100
      phpunit/src/FixedReferenceStorageTest.php
  18. 33
      phpunit/src/TypedReferenceTypeTest.php
  19. 12
      src/Analysis/SsaBuilder.php
  20. 35
      src/CompilerBase.php
  21. 23
      src/Context/CompilationStateTrait.php
  22. 16
      src/Context/FunctionContext.php
  23. 767
      src/Generator/CallArgumentGenerator.php
  24. 20
      src/Generator/ClosureGenerator.php
  25. 3
      src/Generator/TypeCheckGenerator.php
  26. 37
      src/NativeClass/NativeClassSupportTrait.php
  27. 25
      src/NativeClass/NativeGlobalTypeResolver.php
  28. 14
      src/Optimizer/SsaPropOptimizer.php
  29. 16
      src/Parser/ArrayExpressionTrait.php
  30. 81
      src/Parser/AssignOpTrait.php
  31. 2
      src/Parser/FunctionCallTrait.php
  32. 6
      src/Parser/PropertyAccessTrait.php
  33. 11
      src/Parser/TypeConversionTrait.php
  34. 53
      src/Preprocessor.php
  35. 35
      src/PythonTools/Converter/PythonToTypePhpConverter.php
  36. 11
      src/Resolver/ClassConstantValueTrait.php
  37. 48
      src/Testing/TestCoverageAnalyzer.php
  38. 37
      src/Translator.php
  39. 45
      src/Type.php
  40. 45
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  41. 134
      src/gen_stub.php
  42. 12
      tests/compiler/array_method/method.phpt
  43. 8
      tests/compiler/attribute/immutable.phpt
  44. 4
      tests/compiler/basic/pow-int-overflow.phpt
  45. 2
      tests/compiler/basic/unset.phpt
  46. 2
      tests/compiler/coalesce/unset-ref-coalesce.phpt
  47. 4
      tests/compiler/const/php-int-max-namespace-shadow.phpt
  48. 4
      tests/compiler/magic_methods/misc.phpt
  49. 2
      tests/compiler/object_property/native-int-property-assign-op-var.phpt
  50. 2
      tests/compiler/object_property/native-int-property-assign.phpt
  51. 2
      tests/compiler/operator/concat_empty.phpt
  52. 1
      tests/compiler/operator/constant-shift-boundaries.phpt
  53. 1
      tests/compiler/operator/literal-division-by-zero-runtime.phpt
  54. 2
      tests/compiler/operator/literal-division-by-zero-typed-slots.phpt
  55. 1
      tests/compiler/operator/typed-int-mod-shift.phpt
  56. 6
      tests/compiler/operator/typed-vs-explicit-native-arith.phpt
  57. 1
      tests/compiler/optimizations/final-int-property-add-chain.phpt
  58. 4
      tests/compiler/optimizations/float-bitwise-mod.phpt
  59. 4
      tests/compiler/optimizations/float-narrow-mixed.phpt
  60. 4
      tests/compiler/optimizations/float-narrow-nested-int-ops.phpt
  61. 4
      tests/compiler/optimizations/int-calculation.phpt
  62. 22
      tests/compiler/ref/010.phpt
  63. 15
      tests/compiler/ref/011.phpt
  64. 6
      tests/compiler/ref/nested-reference-return.phpt
  65. 2
      tests/compiler/ref/property-reference-rebind.phpt
  66. 10
      tests/compiler/ref/ref-call-arg.phpt
  67. 6
      tests/compiler/ref/ref-object.phpt
  68. 64
      tests/compiler/ref/typed-native-call-forms.phpt
  69. 103
      tests/compiler/ref/typed-native-local-reference.phpt
  70. 106
      tests/compiler/ref/typed-property-and-array-reference.phpt
  71. 177
      tests/compiler/ref/typed-reference-dynamic-bridge.phpt
  72. 2
      tests/compiler/static/static-property-function-local-slot.phpt
  73. 4
      tests/compiler/type_hits/native-type.phpt
  74. 21
      tests/compiler/variadic/by-reference-closure-callback.phpt
  75. 2
      tests/compiler/variadic/by-reference-types.phpt

@ -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` 会被拒绝。

@ -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.

@ -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。
## 对象模型

@ -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<php::Str> bridge(property);
php_update_name(bridge.typed());
```
构造时必须验证当前值类型。`Int/Float` 可以直接引用 reference 内部 `zval`
payload;`Str/Array` 使用安全的 indirect wrapper,使原生函数的每次写入仍作用于
原始槽位。Zend 的 `bool` 只编码为 `IS_TRUE/IS_FALSE` 类型标签,并不存在可取地址
的 `bool` payload,因此 `Bool&` 必须使用栈上代理,并在本次调用结束时写回。
typed-ref 函数只能写入 `T`,因此不会破坏属性类型。
构造时必须验证当前值类型。跨 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<T>` 为 typed local 创建临时 `php::Ref`:
`std::ref()` 时,`php::RefWrap<T>` 为 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<T>` 有两种互斥状态:
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<php::Int> 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 设计复杂度。
## 实施顺序

@ -0,0 +1,9 @@
<?php
function typedReferenceFloatArgument(float &$value): void {}
function typedReferenceArgumentTypeError(): void
{
$value = 1;
typedReferenceFloatArgument($value);
}

@ -0,0 +1,8 @@
<?php
function typedReferenceArrayEscape(): void
{
$value = 1;
$array = [];
$array[] =& $value;
}

@ -0,0 +1,8 @@
<?php
function typedReferenceAssignmentTypeError(): void
{
$value = 100;
$reference =& $value;
$reference = '100';
}

@ -0,0 +1,9 @@
<?php
function typedReferenceConditionalBind(bool $condition): void
{
$value = 1;
if ($condition) {
$alias =& $value;
}
}

@ -0,0 +1,8 @@
<?php
final class TypedReferenceObjectClosureParamValue {}
function typedReferenceObjectClosureParam(): void
{
$closure = function (TypedReferenceObjectClosureParamValue &$value): void {};
}

@ -0,0 +1,5 @@
<?php
final class TypedReferenceObjectParamValue {}
function typedReferenceObjectParam(TypedReferenceObjectParamValue &$value): void {}

@ -0,0 +1,6 @@
<?php
function &typedReferenceParameterReturn(int &$value): mixed
{
return $value;
}

@ -0,0 +1,13 @@
<?php
final class TypedReferencePropertyEscapeHolder
{
public mixed $value;
}
function typedReferencePropertyEscape(): void
{
$value = 1;
$holder = new TypedReferencePropertyEscapeHolder();
$holder->value =& $value;
}

@ -0,0 +1,9 @@
<?php
function typedReferenceRebind(): void
{
$first = 1;
$second = 2;
$alias =& $first;
$alias =& $second;
}

@ -0,0 +1,9 @@
<?php
function typedReferenceRootRebind(): void
{
$first = 1;
$second = 2;
$alias =& $first;
$first =& $second;
}

@ -0,0 +1,8 @@
<?php
function typedReferenceUnsetAlias(): void
{
$value = 1;
$alias =& $value;
unset($alias);
}

@ -0,0 +1,8 @@
<?php
function typedReferenceUnsetRoot(): void
{
$value = 1;
$alias =& $value;
unset($value);
}

@ -24,20 +24,40 @@ final class FixedReferenceStorageTest extends BaseTest
yield 'std container' => ['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<php::Array>', $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;

@ -0,0 +1,33 @@
<?php
use PHPUnit\Framework\TestCase;
use TypePhp\Type;
final class TypedReferenceTypeTest extends TestCase
{
public function testValueAndReferenceTypeMappingsAreSymmetric(): void
{
$types = [
Type::INT => 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));
}
}

@ -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;
}
/**

@ -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

@ -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])) {

@ -72,6 +72,14 @@ class FunctionContext
public array $explicitNativeTypeVars = [];
/** @var array<string, string> C++ initializers folded into function-scope local declarations. */
public array $localVarInitializers = [];
/** @var array<string, string> typed-ref local => directly referenced local. */
public array $typedRefBindings = [];
/** @var array<string, string> typed-ref local => canonical fixed-storage root. */
public array $typedRefRoots = [];
/** @var array<string, array<string, true>> canonical root => aliases. */
public array $typedRefAliases = [];
/** @var list<array<string, string>> 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;

@ -1,14 +1,13 @@
<?php
/**
* This file is part of TypePHP.
* This file is part of TypePHP(AOT).
*
* Call argument lowering shared by native and dynamic call paths.
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
namespace TypePhp\Generator;
use TypePhp\Type;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
@ -16,7 +15,7 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\FunctionDef;
use TypePhp\Exception\PlaceHolder;
use TypePhp\Resolver\Reflection;
use TypePhp\Generator\Symbol;
use TypePhp\Type;
trait CallArgumentGenerator
{
@ -28,203 +27,207 @@ trait CallArgumentGenerator
string $nativeFunc,
int $parameterOffset = 0,
bool $deferTrailingDefaults = false,
): string
{
): string {
$this->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);
}
}

@ -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();

@ -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.

@ -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<string, ClassDef> $byName
* @param list<ClassDef> $ordered
* @param array<string, bool> $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();

@ -13,6 +13,12 @@ use PhpParser\Node;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef;
final class NativeGlobalConstantVisitState
{
/** @var array<string, true> */
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<string, true> $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;

@ -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;
}

@ -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 . ');';

@ -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;

@ -37,7 +37,7 @@ trait FunctionCallTrait
$callable = $this->parseExprAsValue($expr->right);
return 'typephp_call_cached(' . $callable . ', ' . $this->getFunctionCallCache()
. ', {' . $value . '})';
. ', php::VarList{' . $value . '})';
}
/**

@ -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);

@ -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,

@ -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

@ -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<string, mixed> $target
* @param array<string, mixed> $node
* @param list<array<string, mixed>> $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<string, mixed> $node */
private function expression(array $node): string
{

@ -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;
}

@ -573,25 +573,27 @@ final class TestCoverageAnalyzer
/** @param Node[] $nodes @return list<Node\Stmt\ClassMethod> */
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<array{name: string, node: Expr\MethodCall}> */
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<Node> $ancestors @param array<string, true> $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<string, true> $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

@ -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']],

@ -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,
};
}
}

@ -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);
}

@ -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;

@ -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);

@ -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

@ -2,6 +2,8 @@
pow int overflow
--FILE--
<?php
use varint_types;
function main()
{
$a = 2 ** 80;
@ -9,4 +11,4 @@ function main()
}
?>
--EXPECT--
1.2089258196146292E+24
1.2089258196146292E+24

@ -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));

@ -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);

@ -4,6 +4,8 @@ Namespaced PHP_INT_MAX shadows the global constant in unqualified fetches
<?php
namespace N {
use varint_types;
const PHP_INT_MAX = 5;
function shadowed(): int
@ -18,6 +20,8 @@ namespace N {
}
namespace {
use varint_types;
function main(): void
{
var_dump(\N\shadowed());

@ -2,6 +2,8 @@
Magic Methods - __get, __set, __call, __invoke etc.
--FILE--
<?php
use varint_types;
class Point {
public function __construct(
private float $x = 0.0,
@ -66,4 +68,4 @@ float(10)
float(10)
float(20)
string(4) "test"
int(100)
int(100)

@ -2,6 +2,8 @@
Native int property compound assignment from var
--FILE--
<?php
use varint_types;
class NativeIntAssignOpBox
{
public int $value = 1;

@ -2,6 +2,8 @@
Native int property assignment follows strict property type rules
--FILE--
<?php
use varint_types;
class GridSize
{
public int $size = 0;

@ -13,7 +13,7 @@ function main() {
var_dump('' . $value . '');
var_dump('' . get_value() . '');
$number = 1;
$number = std::any(1);
$number .= '';
var_dump($number);
}

@ -2,6 +2,7 @@
Constant bit shift boundaries follow PHP semantics
--FILE--
<?php
use varint_types;
function main(): void
{

@ -2,6 +2,7 @@
Literal zero divisors compile and raise catchable DivisionByZeroError at runtime
--FILE--
<?php
use varint_types;
function main(): void
{

@ -2,6 +2,8 @@
Literal zero divisors on typed native slots raise catchable DivisionByZeroError
--FILE--
<?php
use varint_types;
function divInt(int $value): mixed
{
try {

@ -2,6 +2,7 @@
Typed int modulo and shifts follow PHP semantics (errors, boundaries)
--FILE--
<?php
use varint_types;
function modInts(int $a, int $b): int
{

@ -1,13 +1,15 @@
--TEST--
Ordinary typed scalars use PHP arithmetic; std::int()/std::float() stay native
varint_types typed scalars use PHP arithmetic; std::int()/std::float() stay native
--FILE--
<?php
use varint_types;
function phpDiv(int $a, int $b): mixed { return $a / $b; }
function phpMod(int $a, int $b): mixed { return $a % $b; }
function main(): void
{
// Ordinary typed parameters follow PHP semantics.
// varint_types selects PHP arithmetic for ordinary typed parameters.
var_dump(phpDiv(7, 2));
var_dump(phpMod(-7, 2));

@ -2,6 +2,7 @@
Final int property addition uses a detached value accumulator
--FILE--
<?php
use varint_types;
final class AddChain
{

@ -4,12 +4,12 @@ SSA narrowing: bitwise/mod on float prevents narrowing
<?php
function main(): void {
// %= on float: PHP converts to int → $a stays Var
$a = 10.5;
$a = std::any(10.5);
$a %= 3;
var_dump($a);
// |= on float: PHP converts to int → $b stays Var
$b = 6.7;
$b = std::any(6.7);
$b |= 2;
var_dump($b);

@ -4,12 +4,12 @@ SSA narrowing: mixed int/float types stay Var
<?php
function main(): void {
// Mixed definitions: int then float → stays Var
$a = 10;
$a = std::any(10);
$a = 3.14;
var_dump($a);
// Float then int → stays Var
$b = 2.5;
$b = std::any(2.5);
$b = 99;
var_dump($b);

@ -3,11 +3,11 @@ SSA narrowing: nested integer-only ops on float prevent narrowing
--FILE--
<?php
function main(): void {
$x = 6.7;
$x = std::any(6.7);
$y = $x & 3;
var_dump($y);
$z = 10.5;
$z = std::any(10.5);
var_dump($z % 4);
}
?>

@ -2,6 +2,8 @@
SSA: int
--FILE--
<?php
use varint_types;
function main(): void {
ini_set('precision', 17);
$a = 100;
@ -16,4 +18,4 @@ function main(): void {
?>
--EXPECTF--
12
9.2233720368547758E+18
9.2233720368547758E+18

@ -0,0 +1,22 @@
--TEST--
class const 001
--FILE--
<?php
function foo2(int &$v) : int {
$v += 30;
return $v;
}
function foo3($v1, int &$v2): int {
$v2 *= 2;
return $v1 + $v2;
}
function main()
{
$i = 100;
$f = 'foo2';
$r = foo3($f(std::ref($i)), $i);
var_dump($r);
}
?>
--EXPECT--
int(390)

@ -0,0 +1,15 @@
--TEST--
class const 001
--FILE--
<?php
function foo2(int &$v) : int {
$v += 30;
return $v;
}
function main()
{
eval('$i = 100; $r = foo2($i); var_dump($r);');
}
?>
--EXPECT--
int(130)

@ -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"

@ -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;
}

@ -1,5 +1,11 @@
--TEST--
ref call arg
--SKIPIF--
<?php
if (!class_exists('ZipArchive')) {
die('skip zip extension is required');
}
?>
--FILE--
<?php
function main()
@ -14,8 +20,10 @@ function main()
$zip->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();

@ -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--
<?php
class Test
@ -12,7 +12,7 @@ class Test
}
}
function test1(Test &$test): void
function test1(mixed &$test): void
{
var_dump($test->value);
var_dump($test->abc());

@ -0,0 +1,64 @@
--TEST--
Typed-reference ABI supports functions, methods, constructors, interfaces, and named arguments
--FILE--
<?php
interface TypedReferenceMutator
{
public function mutate(string &$value, int $amount): void;
}
final class TypedReferenceTarget implements TypedReferenceMutator
{
public function __construct(int &$value)
{
$value += 10;
}
public function mutate(string &$value, int $amount): void
{
$value .= ':' . $amount;
}
public static function toggle(bool &$value): void
{
$value = !$value;
}
}
function widenFloat(float &$value): void
{
// Assigning an integer to a float reference writes a float value.
$value = 7;
}
function mutateThroughInterface(TypedReferenceMutator $target, string &$value): void
{
$target->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)

@ -0,0 +1,103 @@
--TEST--
Native typed local references preserve alias and value-copy semantics
--FILE--
<?php
function increment(int &$value): void
{
$value++;
}
function appendSuffix(string &$value): void
{
$value .= ':changed';
}
function halve(float &$value): void
{
$value /= 2.0;
}
function toggle(bool &$value): void
{
$value = !$value;
}
function appendNumber(array &$value): void
{
$value[] = 3;
}
function main(): void
{
$a = 100;
$b =& $a;
$c = $b;
$d =& $b;
$b = 101;
$d = 102;
$c = 200;
increment($d);
var_dump($a, $b, $c, $d);
$string = 'value';
$stringRef =& $string;
$stringCopy = $stringRef;
appendSuffix($stringRef);
var_dump($string, $stringRef, $stringCopy);
$float = 8.0;
$floatRef =& $float;
halve($floatRef);
var_dump($float, $floatRef);
$bool = false;
$boolRef =& $bool;
toggle($boolRef);
var_dump($bool, $boolRef);
$array = [1, 2];
$arrayRef =& $array;
$arrayCopy = $arrayRef;
appendNumber($arrayRef);
$arrayCopy[] = 4;
var_dump($array, $arrayRef, $arrayCopy);
}
?>
--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)
}

@ -0,0 +1,106 @@
--TEST--
Typed properties retain Zend reference type sources while PHP array elements remain dynamic
--FILE--
<?php
final class TypedReferenceHolder
{
public string $value = 'instance';
public static string $staticValue = 'static';
}
function appendSuffix(string &$value): void
{
$value .= ':changed';
}
function replaceWithArray(mixed &$value): void
{
$value = ['invalid'];
}
function appendThenThrow(string &$value): void
{
$value .= ':throw';
throw new RuntimeException('stop');
}
function main(): void
{
$holder = new TypedReferenceHolder();
appendSuffix($holder->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"

@ -0,0 +1,177 @@
--TEST--
Typed native references bridge safely across mixed and dynamic Zend calls
--FILE--
<?php
function replaceMixed(mixed &$value, mixed $replacement): void
{
$value = $replacement;
}
function observePair(mixed $first, mixed $second): void
{
var_dump($first, $second);
}
function runtimeValue(bool $invalid): mixed
{
return $invalid ? 'invalid' : 4;
}
function incrementTyped(int &$value): int
{
return ++$value;
}
function appendMixed(mixed &$value, int $item): int
{
$value[] = $item;
return count($value);
}
function main(): void
{
$int = 1;
replaceMixed($int, 2);
var_dump($int);
try {
replaceMixed($int, 'invalid');
} catch (TypeError $error) {
echo "type error\n";
}
var_dump($int);
$typedAlias =& $int;
$typedAlias = runtimeValue(false);
try {
$typedAlias = runtimeValue(true);
} catch (TypeError $error) {
echo "assignment type error\n";
}
var_dump($int, $typedAlias);
// A Var -> 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)
}
}

@ -61,7 +61,7 @@ array(3) {
[1]=>
NULL
[2]=>
&string(9) "reference"
string(9) "reference"
}
string(9) "reference"
string(11) "child-write"

@ -37,6 +37,6 @@ float(100)
bool(true)
int(99)
float(2026)
float(2.5)
int(2)
float(10)
int(2)
float(10)

@ -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;

@ -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++;

Loading…
Cancel
Save