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. 162
      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. 151
      src/Generator/CallArgumentGenerator.php
  24. 20
      src/Generator/ClosureGenerator.php
  25. 3
      src/Generator/TypeCheckGenerator.php
  26. 25
      src/NativeClass/NativeClassSupportTrait.php
  27. 25
      src/NativeClass/NativeGlobalTypeResolver.php
  28. 14
      src/Optimizer/SsaPropOptimizer.php
  29. 16
      src/Parser/ArrayExpressionTrait.php
  30. 75
      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. 29
      src/PythonTools/Converter/PythonToTypePhpConverter.php
  36. 11
      src/Resolver/ClassConstantValueTrait.php
  37. 48
      src/Testing/TestCoverageAnalyzer.php
  38. 35
      src/Translator.php
  39. 45
      src/Type.php
  40. 45
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  41. 102
      src/gen_stub.php
  42. 12
      tests/compiler/array_method/method.phpt
  43. 8
      tests/compiler/attribute/immutable.phpt
  44. 2
      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. 2
      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. 2
      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. 4
      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. 2
      tests/compiler/type_hits/native-type.phpt
  74. 11
      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 Projects must remove `use native_types` and add the new compatibility mode only
to files that genuinely depend on Zend integer widening semantics. to files that genuinely depend on Zend integer widening semantics.
Fixed-storage locals can no longer be converted to PHP references. This rule Fixed `int`, `float`, `bool`, `string`, and `array` locals now use typed native
applies to native scalars, strings, arrays, objects, streams, high-precision references. A stable local alias and an exact by-reference parameter on a
values, and `std` containers. A reference made from an ordinary local has no statically resolved TypePHP call lower directly to C++ `T&`; this path does not
Zend type source, so exposing fixed C++ storage through it could corrupt the box the value or allocate a Zend reference. Bindings must be unconditional,
variable's type. Typed object/static properties remain reference-capable: Zend one-time, function-local, and non-escaping. Rebinding, `unset`, reference
records their property metadata as a type source. PHP array elements remain capture/return, and storing such a reference into PHP storage are rejected.
dynamic reference-capable slots. Use `std::any()` when a local needs reference
semantics and explicitly convert the result back afterward. 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 TypePHP is always strict. Project sources no longer need
`declare(strict_types=1)`; the directive remains accepted as a redundant PHP `declare(strict_types=1)`; the directive remains accepted as a redundant PHP
@ -100,11 +107,16 @@ should review the change log and run their full test suite before upgrading.
`$i = std::any(100)` 或在文件中声明 `use varint_types`。升级项目必须删除 `$i = std::any(100)` 或在文件中声明 `use varint_types`。升级项目必须删除
`use native_types`,并且只为真正依赖 Zend 整数扩展语义的文件添加新兼容模式。 `use native_types`,并且只为真正依赖 Zend 整数扩展语义的文件添加新兼容模式。
固定存储的局部变量不再允许转换为 PHP 引用,包括原生标量、字符串、数组、对象、 固定 `int`、`float`、`bool`、`string`、`array` 局部变量现在使用强类型原生引用。
stream、高精度值和 `std` 容器。普通局部变量产生的引用没有 Zend type source,若允许 稳定的局部别名,以及静态可解析 TypePHP 调用上的精确引用参数,会直接生成 C++ `T&`,
其指向固定 C++ 存储,可能破坏变量类型。Typed Property 仍然可以取引用:Zend 会把属性 不装箱、不创建 Zend reference。绑定必须位于函数顶层、只发生一次且不得逃逸;重新绑定、
元数据记录为 type source;PHP 数组元素也仍是可取引用的动态槽位。局部变量需要引用 `unset`、引用捕获/返回,或把引用存入 PHP 槽位都会在编译期拒绝。
语义时先使用 `std::any()`,操作完成后再显式转换回目标类型。
动态调用与 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)`。该声明仍作为 TypePHP 始终使用严格类型,项目源码不再需要 `declare(strict_types=1)`。该声明仍作为
冗余的 PHP 兼容语法被接受,而 `strict_types=0` 会被拒绝。 冗余的 PHP 兼容语法被接受,而 `strict_types=0` 会被拒绝。

@ -103,17 +103,25 @@ incompatible with or more restrictive than standard PHP.
used explicitly. used explicitly.
- `std::ref()` / `toRef()` only accept variables, array elements, or object - `std::ref()` / `toRef()` only accept variables, array elements, or object
properties. properties.
- A local with fixed storage (`Int`, `Float`, `Bool`, `Str`, `Array`, `Object`, - Fixed `int`, `float`, `bool`, `string`, and `array` locals support a restricted
`Stream`, high-precision values, or a `std` container) cannot be made into a native-reference model. A one-time top-level binding such as `$alias =& $value`
PHP reference. An ordinary local reference has no Zend type source and could becomes a C++ `T&`, and an exact `int/string/float/bool/array &$arg` on a
replace such storage with an incompatible value. Objects, streams, typed statically resolved TypePHP call also uses `T&` without boxing or allocating a
objects, and `std` containers already use handle/reference-like value Zend reference. Rebinding, conditional/loop-local first binding, `unset`,
semantics, so adding a PHP reference to the local variable is unnecessary as by-reference Closure capture, returning the local by reference, or storing the
well. Typed object/static properties remain reference-capable because Zend reference in a property/array/global is rejected because the C++ reference may
attaches their property metadata to the reference; PHP array elements remain not escape or change its target.
dynamic reference-capable slots. Initialize a local with `std::any()` when - Dynamic calls and Closure calls still require explicit `std::ref()` / `toRef()`.
PHP reference semantics are required, then convert it back explicitly with a TypePHP creates a call-scoped Zend reference, validates its type on write-back,
keyword such as `toArray()` or `toString()`. 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 - A call that uses argument unpacking followed by named arguments falls back to
dynamic dispatch and cannot use the native call path. dynamic dispatch and cannot use the native call path.

@ -48,13 +48,20 @@
- 引用赋值不支持从复杂静态属性表达式建立引用。 - 引用赋值不支持从复杂静态属性表达式建立引用。
- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。 - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `std::ref()` 或等价关键词方法 `toRef()`。
- `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。 - `std::ref()` / `toRef()` 只接受变量、数组元素或对象属性。
- 采用固定存储的局部变量(`Int`、`Float`、`Bool`、`Str`、`Array`、`Object`、 - 固定 `int`、`float`、`bool`、`string`、`array` 局部变量支持受限的原生引用模型。
`Stream`、高精度值或 `std` 容器)不能转换为 PHP 引用。普通局部引用没有 Zend `$alias =& $value` 这类函数顶层的一次性绑定会生成 C++ `T&`;静态可解析的 TypePHP
type source,可能把固定存储替换成不兼容的值。对象、stream、typed object 与 调用中,精确的 `int/string/float/bool/array &$arg` 同样直接传递 `T&`,不装箱也不
`std` 容器本身已采用句柄或引用式值语义,再对局部变量建立 PHP 引用也没有意义。 创建 Zend reference。由于 C++ 引用不能改绑或逃逸,条件/循环内首次绑定、重新绑定、
Typed Property 仍可取引用,Zend 会将属性元数据附加到引用上;PHP 数组元素仍是 `unset`、Closure 按引用捕获、按引用返回局部变量,以及把引用保存到属性、数组或全局
可取引用的动态槽位。局部变量确需 PHP 引用语义时,应使用 `std::any()` 初始化, 槽位均会在编译期拒绝。
完成引用操作后再通过 `toArray()`、`toString()` 等关键词显式转换回来。 - 动态函数、动态方法和 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。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。
## 对象模型 ## 对象模型

@ -43,7 +43,6 @@ php::Int &alias = value;
- `isTypedRefType($type)`:是否为五种 typed-ref; - `isTypedRefType($type)`:是否为五种 typed-ref;
- `getReferenceType($type)`:值类型转为 typed-ref; - `getReferenceType($type)`:值类型转为 typed-ref;
- `getReferencedType($type)`:typed-ref 转为值类型; - `getReferencedType($type)`:typed-ref 转为值类型;
- `getReferenceCppType($type)`:生成 `T &` ABI;
- `isAnyRefType($type)`:typed-ref 或动态 `REF`。 - `isAnyRefType($type)`:typed-ref 或动态 `REF`。
SSA 只能保留或缩窄引用目标类型,不得把 typed-ref 提升成 `VAR/REF`。 SSA 只能保留或缩窄引用目标类型,不得把 typed-ref 提升成 `VAR/REF`。
@ -108,16 +107,21 @@ php::RefWrap<php::Str> bridge(property);
php_update_name(bridge.typed()); php_update_name(bridge.typed());
``` ```
构造时必须验证当前值类型。`Int/Float` 可以直接引用 reference 内部 `zval` 构造时必须验证当前值类型。跨 Zend 边界的性能不属于 typed-ref 的零成本目标,因此
payload;`Str/Array` 使用安全的 indirect wrapper,使原生函数的每次写入仍作用于 五种类型统一使用隔离代理,不直接暴露 `zend_reference` 内部 payload。这样即使原始
原始槽位。Zend 的 `bool` 只编码为 `IS_TRUE/IS_FALSE` 类型标签,并不存在可取地址 reference 在 typed-ref 函数重入 ZendVM 时被其他动态别名改成另一种类型,也不会让
的 `bool` payload,因此 `Bool&` 必须使用栈上代理,并在本次调用结束时写回。 `T&` 指向已经失效的 zval union member。
typed-ref 函数只能写入 `T`,因此不会破坏属性类型。
调用结束时比较入口快照、原始 reference 和代理:只有一侧发生修改时执行安全写回;
双方发生不同修改时抛出冲突错误,不能静默覆盖。`Array` 还必须比较 HashTable 身份,
因为“返回数组元素引用”会改变引用拓扑,即使数组值比较结果完全相同。数组代理使用
独立副本,避免 persistent/default HashTable 与 request 写路径混用;字符串代理也必须
持有 request-owned 存储,不能原地修改 persistent string。
## typed-ref 转换为动态 PHP 引用 ## typed-ref 转换为动态 PHP 引用
动态函数、动态方法和 Closure 的引用签名在编译期不可确定。调用者显式使用 动态函数、动态方法和 Closure 的引用签名在编译期不可确定。调用者显式使用
`std::ref()` 时,`php::RefWrap<T>` 为 typed local 创建临时 `php::Ref`: `std::ref()` 时,`php::RefWrap<T>` 为 typed local 提供真实的 `php::Ref`:
```cpp ```cpp
php::Int value = 1; php::Int value = 1;
@ -128,6 +132,8 @@ bridge.commit();
`commit()` 必须检查动态调用后的 zval 仍与 `T` 一致,然后写回 typed local; `commit()` 必须检查动态调用后的 zval 仍与 `T` 一致,然后写回 typed local;
类型不一致时抛出统一的 TypeError。不能依赖一个可能抛异常的析构函数完成检查。 类型不一致时抛出统一的 TypeError。不能依赖一个可能抛异常的析构函数完成检查。
这是明确的动态慢路径,可以继续按调用创建 Zend reference,不为减少分配引入函数级
缓存、复用状态或额外的引用身份管理。
桥接生命周期必须是“一次调用表达式”,不能是整个 PHP 语句。嵌套调用中,内层 桥接生命周期必须是“一次调用表达式”,不能是整个 PHP 语句。嵌套调用中,内层
调用返回后,必须在求值外层调用的下一个实参之前完成 `commit()`: 调用返回后,必须在求值外层调用的下一个实参之前完成 `commit()`:
@ -136,10 +142,11 @@ bridge.commit();
outer(dynamicCall(std::ref($value)), nextArgument()); outer(dynamicCall(std::ref($value)), nextArgument());
``` ```
这里 `dynamicCall()` 的写回必须先于 `nextArgument()`。因此不能用语句级 这里 `dynamicCall()` 的写回必须先于 `nextArgument()`。生成器可以使用立即执行
`beforeStmtLines/afterStmtLines` 实现,而应把 wrapper、调用、成功写回和异常写回 lambda,也可以捕获并在独立 C++ block 中输出该内层调用的
统一放入一个立即执行的表达式 helper/lambda 中。多个 typed-ref 实参若指向同一变量, `beforeStmtLines/afterStmtLines`;关键是 wrapper、调用和成功/异常写回必须属于同一个
必须复用同一个 bridge,不能创建内容互相覆盖的临时 reference。 求值边界。多个 typed-ref 实参若指向同一变量,必须复用同一个 bridge,不能创建内容
互相覆盖的临时 reference。
调用抛出异常时,桥接层仍要按 PHP 的可观察语义处理已经发生的引用修改。生成代码 调用抛出异常时,桥接层仍要按 PHP 的可观察语义处理已经发生的引用修改。生成代码
必须在重新抛出原异常前执行非破坏性的写回;若写回又发现类型错误,以类型错误替换 必须在重新抛出原异常前执行非破坏性的写回;若写回又发现类型错误,以类型错误替换
@ -150,19 +157,19 @@ Zend 异常处理代码。
`RefWrap<T>` 有两种互斥状态: `RefWrap<T>` 有两种互斥状态:
1. `T& -> php::Ref`:为动态调用创建临时 Zend reference,调用结束后验证并写回; 1. `T& -> php::Ref`:为动态调用创建 Zend reference,调用结束后验证并写回;
2. `php::Ref -> T&`:为静态 typed-ref 参数提供原生引用视图。 2. `php::Ref -> T&`:为静态 typed-ref 参数提供原生引用视图。
建议内部状态明确表示为 `NativeToZend`、`ConstrainedZendToNative`、 内部只区分 `T& -> php::Ref` 与 `php::Ref -> T&` 两种方向;成功提交后必须进入
`DynamicZendToNative`,禁止用若干松散布尔值组合隐含方向。 `committed` 状态,保证显式 `commit()` 与 `noexcept` 析构不会重复写回。
| `T` | `php::Ref -> T&` 实现 | 是否需要写回代理 | | `T` | `php::Ref -> T&` 代理 | 额外要求 |
| --- | --- | --- | | --- | --- | --- |
| `Int` | 直接指向 `Z_LVAL_P(refval)` | 否 | | `Int` | 栈上 `Int` | 精确 `IS_LONG` |
| `Float` | 直接指向 `Z_DVAL_P(refval)` | 否 | | `Float` | 栈上 `Float` | 精确 `IS_DOUBLE`;固定 `Int&` 不隐式改成 `Float&` |
| `Bool` | 栈上 `Bool` 代理 | 是 | | `Bool` | 栈上 `Bool` | `IS_TRUE/IS_FALSE` |
| `Str` | 指向 refval 的 indirect `Str` | 否 | | `Str` | request-owned `Str` | 禁止原地修改 persistent string |
| `Array` | 指向 refval 的 indirect `Array` | 否 | | `Array` | 独立 `Array` 副本 | 同时检测内容与 HashTable/引用拓扑变化 |
`Bool` 不能直接映射的原因来自 Zend ABI:布尔值由 zval 的 `IS_TRUE/IS_FALSE` `Bool` 不能直接映射的原因来自 Zend ABI:布尔值由 zval 的 `IS_TRUE/IS_FALSE`
类型标签表达,没有独立的 `bool` payload。 类型标签表达,没有独立的 `bool` payload。
@ -171,15 +178,10 @@ Zend 异常处理代码。
可能还有其他动态别名;typed 函数内部若发生重入调用,动态代码可能把同一个 refval 可能还有其他动态别名;typed 函数内部若发生重入调用,动态代码可能把同一个 refval
改成其他类型。随后继续通过 `Int&/Float&` 访问已经不再活动的 union member 是不安全的。 改成其他类型。随后继续通过 `Int&/Float&` 访问已经不再活动的 union member 是不安全的。
因此 `php::Ref -> T&` 分成两种安全级别: 因此 `php::Ref -> T&` 不按 type source 选择不同内存模型。Typed Property 的 type
source 仍由 `Reference::operator=()` 在提交时执行 Zend 类型约束,但它不成为暴露裸
- reference 带有与 `T` 一致的 Zend typed-property type source:Zend 会阻止重入代码 payload 的理由。统一代理牺牲的是动态桥接路径的少量性能,换取更简单、可验证的生命
改变类型,`Int/Float/Str/Array` 可以直接映射; 周期规则;静态 TypePHP `T& -> T&` 主路径完全不经过这里。
- 普通无 type source 的 reference:必须使用隔离代理。调用结束时比较原始 reference
与入口快照,检测重入别名修改;出现双方同时修改的冲突时抛出明确错误,不能静默覆盖。
`Bool` 在两种情况下都使用代理。直接映射前必须检查 reference 的全部 type sources,
不能仅凭当前 `Z_TYPE` 推断其以后不会改变。
### 调用表达式边界 ### 调用表达式边界
@ -200,19 +202,18 @@ outer(first(), second()); // first/second 谁先执行未指定
```cpp ```cpp
auto result = [&]() { auto result = [&]() {
php::RefWrap<php::Int> bridge(value); php::RefWrap<php::Int> bridge(value);
try {
auto retval = dynamic_call(bridge.ref()); auto retval = dynamic_call(bridge.ref());
bridge.commit(); bridge.commit();
return retval; return retval;
} catch (zend_object *) {
bridge.commitAfterException();
throw;
}
}(); }();
``` ```
这里 `commit()` 和 `bridge` 析构都发生在 lambda 产生结果之前。外层 正常返回由显式 `commit()` 完成全部校验;若调用抛出异常,C++ 栈展开会在离开 lambda
`VarList{inner(), next()}` 才能保证 `inner()` 写回先于 `next()`。 前执行 `noexcept` 析构,只提交类型兼容的既有修改并保留原异常。`commitAfterException()`
属于 `RefWrap` 私有实现,生成代码不直接调用它。这里 `commit()` 和 bridge 的析构都在
lambda 产生结果之前。使用生成语句与局部 block 时必须保持相同边界。外层
`VarList{inner(), next()}` 才能保证 `inner()` 写回先于 `next()`;不能依赖外层
full-expression 的析构时机。
### 异常路径 ### 异常路径
@ -233,7 +234,7 @@ C++ 异常。这个规则必须集中在 helper 中,析构函数始终保持 `
### 别名与逃逸 ### 别名与逃逸
同一次动态调用中,如果两个实参最终拥有相同引用根,必须复用一个 `RefWrap`: 同一次动态调用中,如果实参最终拥有相同引用根,必须复用一个 `RefWrap`:
```php ```php
$b =& $a; $b =& $a;
@ -245,10 +246,14 @@ $callback(std::ref($b), std::ref($d));
身份。编译器应按 `typedRefRoots` 对 bridge 去重。 身份。编译器应按 `typedRefRoots` 对 bridge 去重。
动态 PHP 代码还可能把引用保存到全局、对象属性或 Closure 中,使其超过本次调用。 动态 PHP 代码还可能把引用保存到全局、对象属性或 Closure 中,使其超过本次调用。
栈上 native storage 无法安全支持这种逃逸。`RefWrap` 应在调用返回时检测临时 栈上 native storage 无法安全支持这种逃逸。调用参数容器销毁后,`RefWrap` 应检查
`zend_reference` 是否出现额外持有者;检测到逃逸时抛出明确错误。该限制保证不会把 `zend_reference` 是否仍有额外持有者;检测到逃逸时抛出明确错误。该 reference 内只
已经失效的 C++ 地址暴露给 ZendVM。需要长期 PHP 引用身份的变量必须从一开始使用 保存 native 值的副本,不保存 native 地址,因此不能形成悬空指针。需要长期 PHP 引用
`std::any()`/`php::Ref`。 身份的变量必须从一开始使用 `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++ 类型; 1. **固定类型不变**:typed-ref 的目标在整个生命周期内始终是同一个 C++ 类型;
2. **绑定不变**:一个 C++ reference 构造后永不重新绑定; 2. **绑定不变**:一个 C++ reference 构造后永不重新绑定;
3. **根唯一**:引用链被压缩到唯一 root,同一次调用中一个 root 只创建一个 bridge; 3. **根唯一**:编译期已知的 typed-local 引用链被压缩到唯一 root,同一次调用中一个
已知 root 只创建一个 bridge;
4. **只求值一次**:属性 receiver、数组 key、动态 callable 和每个参数表达式都不得重复求值; 4. **只求值一次**:属性 receiver、数组 key、动态 callable 和每个参数表达式都不得重复求值;
5. **PHP 求值顺序**:参数及嵌套调用按照 PHP 的从左到右可观察顺序完成; 5. **PHP 求值顺序**:参数及嵌套调用按照 PHP 的从左到右可观察顺序完成;
6. **调用级提交**:bridge 在本次调用完成后、下一个表达式开始前提交; 6. **调用级提交**:bridge 在本次调用完成后、下一个表达式开始前提交;
@ -368,29 +374,72 @@ reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 `
## 性能模型 ## 性能模型
不能把正确性建立在“GCC 可能优化掉”之上。生成结构本身应让常见静态路径保持零成本, 性能要求只覆盖编译期可确定的 TypePHP 原生调用与 typed-ref。不能把正确性建立在
编译器优化只负责消除栈对象和内联 helper。 “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 local -> TypePHP typed-ref 参数 | 0 | 直接 `T&`,与普通 C++ 调用一致 |
| typed-ref local/参数 -> TypePHP typed-ref 参数 | 0 | 直接转发相同 `T&` | | typed-ref local/参数 -> TypePHP typed-ref 参数 | 0 | 直接转发相同 `T&` |
| local typed alias | 0 | C++ reference,优化后通常不占独立存储 | | 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 自身也需要 | | 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++ 栈上会产生释放非法地址或悬空 引用计数并暂存该 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; - `RefWrap` 是 header-only、非虚类,不使用 `std::function` 或 type erasure;
- wrapper、tuple/array 与立即执行 lambda 只使用栈存储; - 静态 typed-ref 主路径只使用 C++ `T&`,不得创建 wrapper 或 Zend reference;
- 同一调用按引用根去重,避免重复建立 `zend_reference`; - 动态/var-ref 路径可继续使用既有 `Args`、Zend reference 与动态内存分配,不为其性能
引入函数级缓存、跨调用复用或裸指针旁路;
- 同一调用中可静态证明同根的 typed-local 实参按引用根去重并共享 reference 身份;
- 成功主路径不创建错误消息或临时字符串,异常检查放入 `UNEXPECTED` 冷分支; - 成功主路径不创建错误消息或临时字符串,异常检查放入 `UNEXPECTED` 冷分支;
- 不在循环或函数级缓存临时 Zend reference,避免引用身份错误和逃逸; - 调用参数容器销毁后立即 `commit()`,不跨调用缓存或延迟同步值;
- 每次提交后验证没有逃逸;
- 普通静态 typed-ref 调用不得生成方向判断、commit 或 refcount 操作; - 普通静态 typed-ref 调用不得生成方向判断、commit 或 refcount 操作;
- `Bool` 代理只用于 Zend bridge,native `Bool& -> Bool&` 仍是直接调用。 - `Bool` 代理只用于 Zend bridge,native `Bool& -> Bool&` 仍是直接调用。
@ -400,15 +449,18 @@ reference/type source;局部 `T&` 又不能安全逃出当前栈帧。因此 `
1. 对纯 native typed-ref micro benchmark 生成 `-O3 -S` 汇编,确认 wrapper 和 lambda 1. 对纯 native typed-ref micro benchmark 生成 `-O3 -S` 汇编,确认 wrapper 和 lambda
不存在、调用可正常 inline; 不存在、调用可正常 inline;
2. 统计静态调用循环的 `malloc/emalloc` 次数,必须为 0; 2. 统计静态 typed-ref 调用循环的额外 `malloc/emalloc` 次数,必须为 0;动态
`std::ref()` 只记录基线,不设零分配目标;
3. 分别 benchmark `Int/Float/Bool/Str/Array` 的直接调用、property/array bridge 和动态 3. 分别 benchmark `Int/Float/Bool/Str/Array` 的直接调用、property/array bridge 和动态
`std::ref()`; `std::ref()`;
4. 比较 `-O0` 生成 C++ 的尺寸,避免每个调用点展开大段异常处理;公共慢路径放在 PHPX 4. 比较 `-O0` 生成 C++ 的尺寸,避免每个调用点展开大段异常处理;公共慢路径放在 PHPX
非模板 helper 中; 非模板 helper 中;
5. 使用 ASan/Valgrind 验证异常、逃逸检测和多别名调用没有泄漏或悬空引用。 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']; 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->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->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 public function testFixedStorageCannotBeReturnedByReference(): void
@ -48,6 +68,14 @@ final class FixedReferenceStorageTest extends BaseTest
$this->compileFixture('fixed-reference-return.php'); $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 public function testFixedStaticStorageCannotBeCapturedByReference(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -56,12 +84,13 @@ final class FixedReferenceStorageTest extends BaseTest
$this->compileFixture('fixed-reference-static.php'); $this->compileFixture('fixed-reference-static.php');
} }
public function testFixedStorageCannotUseToRefKeyword(): void public function testFixedStorageUsesBridgeForToRefAtKnownDynamicReferenceBoundary(): void
{ {
$this->expectException(TestError::class); $code = $this->compileFixture('fixed-reference-to-ref.php');
$this->expectExceptionMessage('variable $value of fixed type php::Array');
$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 public function testExplicitAnyCanUseReferenceStorage(): void
@ -72,6 +101,57 @@ final class FixedReferenceStorageTest extends BaseTest
self::assertStringContainsString('value.toReference()', $code); 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 private function compileFixture(string $fixture): string
{ {
global $translator; 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[] $blocks Reference to the blocks array being built
* @param SsaBlock $currentBlock The current block (already in $blocks) * @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++) { for ($i = 0; $i < count($stmts); $i++) {
$stmt = $stmts[$i]; $stmt = $stmts[$i];
@ -414,7 +414,7 @@ class SsaBuilder
// If statement: expand into condition block + body blocks + join // If statement: expand into condition block + body blocks + join
if ($stmt instanceof Stmt\If_) { if ($stmt instanceof Stmt\If_) {
$this->expandIfStmt($stmt, $blocks, $currentBlock, $i, $stmts); $currentBlock = $this->expandIfStmt($stmt, $blocks, $currentBlock, $i, $stmts);
continue; continue;
} }
@ -431,6 +431,8 @@ class SsaBuilder
// Regular statement: add to current block // Regular statement: add to current block
$currentBlock->stmts[] = $stmt; $currentBlock->stmts[] = $stmt;
} }
return $currentBlock;
} }
/** /**
@ -444,7 +446,7 @@ class SsaBuilder
* *
* @param SsaBlock[] $blocks * @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 // The if statement is the last statement in the current (condition) block
$currentBlock->stmts[] = $ifStmt; $currentBlock->stmts[] = $ifStmt;
@ -473,7 +475,7 @@ class SsaBuilder
$currentBlock = $this->newBlock(); $currentBlock = $this->newBlock();
$blocks[] = $currentBlock; $blocks[] = $currentBlock;
} }
$this->expandIfStmt($innerIf, $blocks, $currentBlock, 0, [$innerIf]); $currentBlock = $this->expandIfStmt($innerIf, $blocks, $currentBlock, 0, [$innerIf]);
} }
} }
if ($ifStmt->else) { if ($ifStmt->else) {
@ -514,6 +516,8 @@ class SsaBuilder
if ($stmtIndex < count($allStmts) - 1) { if ($stmtIndex < count($allStmts) - 1) {
$currentBlock = $joinBlock; $currentBlock = $joinBlock;
} }
return $currentBlock;
} }
/** /**

@ -764,8 +764,11 @@ class CompilerBase implements PropertyAccessContext
if (isset($this->context->declaredObjects[$object])) { if (isset($this->context->declaredObjects[$object])) {
return $this->context->declaredObjects[$object]; return $this->context->declaredObjects[$object];
} }
if (isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object])) { if (isset($this->context->objects[$object])) {
return $this->getObjectType($object); return $this->context->objects[$object];
}
if (isset($this->context->stableObjects[$object])) {
return $this->context->stableObjects[$object];
} }
return ''; return '';
} }
@ -2475,7 +2478,10 @@ class CompilerBase implements PropertyAccessContext
$remainingVariableUses[$name]--; $remainingVariableUses[$name]--;
// Only consume a local on its final occurrence. Globals and // Only consume a local on its final occurrence. Globals and
// statics outlive the function and must never be emptied. // 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 . ')'; $value = 'std::move(' . $value . ')';
} }
} }
@ -5021,6 +5027,29 @@ class CompilerBase implements PropertyAccessContext
$this->fatalError($left, "Cannot re-assign $varName from `{$fromType}` to `{$toType}`"); $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 * 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 * is precise enough to prove a mismatch. Composite declarations still use

@ -22,6 +22,29 @@ trait CompilationStateTrait
$this->context->localVars[$name] = $type; $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 protected function registerStdType(string $key): int
{ {
if (isset($this->stdTypeMap[$key])) { if (isset($this->stdTypeMap[$key])) {

@ -72,6 +72,14 @@ class FunctionContext
public array $explicitNativeTypeVars = []; public array $explicitNativeTypeVars = [];
/** @var array<string, string> C++ initializers folded into function-scope local declarations. */ /** @var array<string, string> C++ initializers folded into function-scope local declarations. */
public array $localVarInitializers = []; 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 $staticVars = [];
public array $globalVars = []; public array $globalVars = [];
@ -129,6 +137,10 @@ class FunctionContext
$this->localVars = []; $this->localVars = [];
$this->explicitNativeTypeVars = []; $this->explicitNativeTypeVars = [];
$this->localVarInitializers = []; $this->localVarInitializers = [];
$this->typedRefBindings = [];
$this->typedRefRoots = [];
$this->typedRefAliases = [];
$this->typedRefBridgeScopes = [];
$this->staticVars = []; $this->staticVars = [];
$this->arguments = []; $this->arguments = [];
$this->immutableVars = []; $this->immutableVars = [];
@ -184,6 +196,10 @@ class FunctionContext
{ {
$this->localVars = $localVars; $this->localVars = $localVars;
$this->localVarInitializers = []; $this->localVarInitializers = [];
$this->typedRefBindings = [];
$this->typedRefRoots = [];
$this->typedRefAliases = [];
$this->typedRefBridgeScopes = [];
$this->tmpVarIndex = $tmpVarIndex; $this->tmpVarIndex = $tmpVarIndex;
$this->declaredObjects = $declaredObjects; $this->declaredObjects = $declaredObjects;
$this->nativeObjects = $nativeObjects; $this->nativeObjects = $nativeObjects;

@ -1,14 +1,13 @@
<?php <?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; namespace TypePhp\Generator;
use TypePhp\Type;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
@ -16,7 +15,7 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\FunctionDef; use TypePhp\Entity\FunctionDef;
use TypePhp\Exception\PlaceHolder; use TypePhp\Exception\PlaceHolder;
use TypePhp\Resolver\Reflection; use TypePhp\Resolver\Reflection;
use TypePhp\Generator\Symbol; use TypePhp\Type;
trait CallArgumentGenerator trait CallArgumentGenerator
{ {
@ -28,9 +27,10 @@ trait CallArgumentGenerator
string $nativeFunc, string $nativeFunc,
int $parameterOffset = 0, int $parameterOffset = 0,
bool $deferTrailingDefaults = false, bool $deferTrailingDefaults = false,
): string ): string {
{
$this->assertCallArgumentLimit($callArgs); $this->assertCallArgumentLimit($callArgs);
$this->context->typedRefBridgeScopes[] = [];
try {
$functionDef = $this->getFunction($nativeFunc); $functionDef = $this->getFunction($nativeFunc);
$providedArgs = []; $providedArgs = [];
$defaultArgs = []; $defaultArgs = [];
@ -225,6 +225,9 @@ trait CallArgumentGenerator
} }
ksort($resolvedArgs); ksort($resolvedArgs);
return implode(', ', $resolvedArgs); return implode(', ', $resolvedArgs);
} finally {
array_pop($this->context->typedRefBridgeScopes);
}
} }
protected function isReferenceArgument($funcName, $className, $argIndex): bool protected function isReferenceArgument($funcName, $className, $argIndex): bool
@ -415,10 +418,15 @@ trait CallArgumentGenerator
string $className = '', string $className = '',
bool $separateNamedArgs = true, bool $separateNamedArgs = true,
bool $forceArrayArgs = false, bool $forceArrayArgs = false,
bool $preserveExistingReferences = false bool $preserveExistingReferences = false,
): string ): string {
{
$this->assertCallArgumentLimit($args); $this->assertCallArgumentLimit($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 = []; $list_args = [];
$arrayArgsVar = null; $arrayArgsVar = null;
$argsVar = null; $argsVar = null;
@ -426,6 +434,16 @@ trait CallArgumentGenerator
$namedArgs = []; $namedArgs = [];
$hasNamedArg = false; $hasNamedArg = false;
$hasUnpack = false; $hasUnpack = false;
$lastHoistingArgIndex = -1;
foreach ($args as $argIndex => $sourceArg) {
if ($sourceArg instanceof Node\Arg
&& !$sourceArg->unpack
&& $this->shouldMaterializeOrderedOperand($sourceArg->value)
) {
$lastHoistingArgIndex = $argIndex;
}
}
if ($forceArrayArgs) { if ($forceArrayArgs) {
$this->ensureCallArrayArgs($arrayArgsVar, $list_args); $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
@ -469,7 +487,7 @@ trait CallArgumentGenerator
} }
$value = ($byRef || $this->isStdRefCall($arg->value) || $this->isToRefCall($arg->value)) $value = ($byRef || $this->isStdRefCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg) ? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg); : $this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex);
$value = $this->wrapScopedCallbackArg($arg, $value); $value = $this->wrapScopedCallbackArg($arg, $value);
if ($separateNamedArgs) { if ($separateNamedArgs) {
$namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar); $namedArgsArray = $this->ensureCallNamedArgs($namedArgsVar);
@ -501,7 +519,10 @@ trait CallArgumentGenerator
$this->fatalError($arg, 'Undefined variable `$' . $name . '`'); $this->fatalError($arg, 'Undefined variable `$' . $name . '`');
} }
} }
$value = $this->wrapScopedCallbackArg($arg, $this->parseCallArgValue($arg)); $value = $this->wrapScopedCallbackArg(
$arg,
$this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex),
);
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs);
continue; continue;
} }
@ -535,7 +556,12 @@ trait CallArgumentGenerator
$this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();'; $this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();';
$this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args, $forceArrayArgs); $this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args, $forceArrayArgs);
} else { } else {
$this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args, $forceArrayArgs); $this->addPositionalCallArg(
$this->materializeCallArgValue($arg->value, $globalVar),
$arrayArgsVar,
$list_args,
$forceArrayArgs,
);
} }
continue; continue;
} }
@ -575,23 +601,33 @@ trait CallArgumentGenerator
continue; continue;
} }
} }
$value = $this->parseCallArgValue($arg); $value = $this->parseOrderedDynamicCallArgValue($arg, $i, $lastHoistingArgIndex);
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs);
} }
if ($argsVar !== null) { if ($argsVar !== null) {
return $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar; $result = $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar;
} } elseif ($arrayArgsVar !== null) {
if ($arrayArgsVar !== null) { $result = $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar;
return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar; } else {
}
// VarList deduces the fixed argument count and owns contiguous // VarList deduces the fixed argument count and owns contiguous
// Variant storage, which PHPX passes directly to Zend without a // Variant storage, which PHPX passes directly to Zend without a
// dynamic php::Args allocation. materializeCallArgValue() above // dynamic php::Args allocation. materializeCallArgValue() above
// ensures that ordinary values do not leave INDIRECT borrows in the // ensures that ordinary values do not leave INDIRECT borrows in the
// list; explicit reference arguments remain references. // list; explicit reference arguments remain references.
$callArgs = Symbol::varList() . '{' . implode(', ', $list_args) . '}'; $callArgs = Symbol::varList() . '{' . implode(', ', $list_args) . '}';
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; $result = $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
}
$this->scheduleCallArgumentCleanup(
$afterStmtStart,
$argsVar,
$arrayArgsVar,
$namedArgsVar,
);
return $result;
} finally {
array_pop($this->context->typedRefBridgeScopes);
}
} }
private function isExistingReferenceCallArg(Node\Arg $arg): bool private function isExistingReferenceCallArg(Node\Arg $arg): bool
@ -642,11 +678,32 @@ trait CallArgumentGenerator
if ($namedArgsVar === null) { if ($namedArgsVar === null) {
$namedArgsVar = $this->genTmpVarName(); $namedArgsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = Type::ARRAY . ' ' . $namedArgsVar . ';'; $this->context->beforeStmtLines[] = Type::ARRAY . ' ' . $namedArgsVar . ';';
$this->context->afterStmtLines[] = $namedArgsVar . '.unset();';
} }
return $namedArgsVar; 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( protected function addPositionalCallArg(
string $value, string $value,
?string $arrayArgsVar, ?string $arrayArgsVar,
@ -682,14 +739,25 @@ trait CallArgumentGenerator
// Those statements are placed before the whole outer call and would // Those statements are placed before the whole outer call and would
// overtake an earlier Call left inside the initializer list. Complete // overtake an earlier Call left inside the initializer list. Complete
// each direct Call in a temporary before lowering the next argument. // each direct Call in a temporary before lowering the next argument.
$expr = $arg->value instanceof Expr\FuncCall $expr = $this->shouldMaterializeOrderedOperand($arg->value)
|| $arg->value instanceof Expr\MethodCall
|| $arg->value instanceof Expr\StaticCall
? $this->parseOrderedArg($arg) ? $this->parseOrderedArg($arg)
: $this->parseArg($arg); : $this->parseArg($arg);
return $this->materializeCallArgValue($arg->value, $expr); 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 protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
{ {
// A Native property fetch is a typed C++ pointer, never an INDIRECT // A Native property fetch is a typed C++ pointer, never an INDIRECT
@ -718,7 +786,8 @@ trait CallArgumentGenerator
return !$this->isStdContainerExpr($value); return !$this->isStdContainerExpr($value);
} }
return $value instanceof Expr\PropertyFetch; return $value instanceof Expr\PropertyFetch
|| $value instanceof Expr\StaticPropertyFetch;
} }
protected function assertCallArgumentLimit(array $args): void protected function assertCallArgumentLimit(array $args): void
@ -844,9 +913,14 @@ trait CallArgumentGenerator
// For a by-reference parameter, an undefined variable may be passed; // For a by-reference parameter, an undefined variable may be passed;
// it is created immediately as a reference // it is created immediately as a reference
$this->addLocalVar($name, Type::REF); $this->addLocalVar($name, Type::REF);
} elseif ($this->getVarType($name) === Type::REF) { } elseif ($this->getRawVarType($name) === Type::REF) {
return '&' . $name; return '&' . $name;
} else { } 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); $this->assertVariableReferenceStorage($arg->value, $arg, $name);
// For a by-reference parameter, use a temporary variable as the reference // For a by-reference parameter, use a temporary variable as the reference
// and replace the actual argument with it // and replace the actual argument with it
@ -861,6 +935,30 @@ trait CallArgumentGenerator
return '&' . $name; 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 protected function parseArg(Node\Arg $arg): string
{ {
if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) { if ($this->isArrayDimFetch($arg->value) and $this->isStdContainerExpr($arg->value)) {
@ -916,5 +1014,4 @@ trait CallArgumentGenerator
} }
return $this->parseExpr($value); return $this->parseExpr($value);
} }
} }

@ -175,6 +175,26 @@ trait ClosureGenerator
'By-reference variadic parameters are not supported on dynamic Closures', '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(); $tmpVar = $this->genTmpVarName();

@ -46,11 +46,13 @@ trait TypeCheckGenerator
protected function isStrictScalarType(string $type): bool protected function isStrictScalarType(string $type): bool
{ {
$type = Type::getReferencedType($type);
return in_array($type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR], true); return in_array($type, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR], true);
} }
protected function strictScalarTypeName(string $type): string protected function strictScalarTypeName(string $type): string
{ {
$type = Type::getReferencedType($type);
return match ($type) { return match ($type) {
Type::INT => 'int', Type::INT => 'int',
Type::FLOAT => 'float', Type::FLOAT => 'float',
@ -62,6 +64,7 @@ trait TypeCheckGenerator
protected function genStrictScalarCondition(string $valueExpr, string $type): string protected function genStrictScalarCondition(string $valueExpr, string $type): string
{ {
$type = Type::getReferencedType($type);
return match ($type) { return match ($type) {
Type::INT => $valueExpr . '.isInt()', Type::INT => $valueExpr . '.isInt()',
// PHP permits int values at a float boundary even in strict mode. // PHP permits int values at a float boundary even in strict mode.

@ -1654,7 +1654,23 @@ trait NativeClassSupportTrait
$ordered = []; $ordered = [];
$visited = []; $visited = [];
$visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void { foreach ($classes as $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), '\\')); $key = strtolower(ltrim($class->getNamespacedName(false), '\\'));
if (isset($visited[$key])) { if (isset($visited[$key])) {
return; return;
@ -1662,14 +1678,9 @@ trait NativeClassSupportTrait
$visited[$key] = true; $visited[$key] = true;
$parent = strtolower(ltrim($class->extends, '\\')); $parent = strtolower(ltrim($class->extends, '\\'));
if ($parent !== '' && isset($byName[$parent])) { if ($parent !== '' && isset($byName[$parent])) {
$visit($byName[$parent]); $this->appendNativeObjectClassInDeclarationOrder($byName[$parent], $byName, $ordered, $visited);
} }
$ordered[] = $class; $ordered[] = $class;
};
foreach ($classes as $class) {
$visit($class);
}
return $ordered;
} }
protected function genNativeObjectDeclarations(): string protected function genNativeObjectDeclarations(): string

@ -13,6 +13,12 @@ use PhpParser\Node;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef; use TypePhp\Entity\ClassDef;
final class NativeGlobalConstantVisitState
{
/** @var array<string, true> */
public array $keys = [];
}
/** /**
* Immutable class metadata used by the Native global pre-pass. * Immutable class metadata used by the Native global pre-pass.
* *
@ -176,7 +182,7 @@ final class NativeGlobalTypeResolver
return $expression->value; return $expression->value;
} }
$visiting = []; $visiting = new NativeGlobalConstantVisitState();
try { try {
$value = $this->evaluateConstantExpression($expression, $scopeClass, $visiting, 0); $value = $this->evaluateConstantExpression($expression, $scopeClass, $visiting, 0);
} catch (\Throwable) { } catch (\Throwable) {
@ -185,11 +191,10 @@ final class NativeGlobalTypeResolver
return is_string($value) ? $value : null; return is_string($value) ? $value : null;
} }
/** @param array<string, true> $visiting */
private function evaluateConstantExpression( private function evaluateConstantExpression(
NodeAbstract $expression, NodeAbstract $expression,
string $scopeClass, string $scopeClass,
array &$visiting, NativeGlobalConstantVisitState $visiting,
int $depth, int $depth,
): mixed { ): mixed {
if ($depth > 32 || !$expression instanceof Node\Expr) { if ($depth > 32 || !$expression instanceof Node\Expr) {
@ -198,7 +203,7 @@ final class NativeGlobalTypeResolver
$evaluator = new ConstExprEvaluator(function (Node\Expr $node) use ( $evaluator = new ConstExprEvaluator(function (Node\Expr $node) use (
$scopeClass, $scopeClass,
&$visiting, $visiting,
$depth, $depth,
): mixed { ): mixed {
if ($node instanceof Node\Expr\ConstFetch) { if ($node instanceof Node\Expr\ConstFetch) {
@ -215,10 +220,10 @@ final class NativeGlobalTypeResolver
} }
if (isset($this->globalConstantExpressions[$name])) { if (isset($this->globalConstantExpressions[$name])) {
$key = 'global:' . $name; $key = 'global:' . $name;
if (isset($visiting[$key])) { if (isset($visiting->keys[$key])) {
throw new \RuntimeException('Circular constant expression'); throw new \RuntimeException('Circular constant expression');
} }
$visiting[$key] = true; $visiting->keys[$key] = true;
try { try {
return $this->evaluateConstantExpression( return $this->evaluateConstantExpression(
$this->globalConstantExpressions[$name], $this->globalConstantExpressions[$name],
@ -227,7 +232,7 @@ final class NativeGlobalTypeResolver
$depth + 1, $depth + 1,
); );
} finally { } finally {
unset($visiting[$key]); unset($visiting->keys[$key]);
} }
} }
if (defined($name)) { if (defined($name)) {
@ -255,10 +260,10 @@ final class NativeGlobalTypeResolver
throw new \RuntimeException('Class constant has no static expression'); throw new \RuntimeException('Class constant has no static expression');
} }
$key = 'class:' . $classKey . '::' . $constant; $key = 'class:' . $classKey . '::' . $constant;
if (isset($visiting[$key])) { if (isset($visiting->keys[$key])) {
throw new \RuntimeException('Circular class constant expression'); throw new \RuntimeException('Circular class constant expression');
} }
$visiting[$key] = true; $visiting->keys[$key] = true;
try { try {
return $this->evaluateConstantExpression( return $this->evaluateConstantExpression(
$constantDefinition->valueExpr, $constantDefinition->valueExpr,
@ -267,7 +272,7 @@ final class NativeGlobalTypeResolver
$depth + 1, $depth + 1,
); );
} finally { } finally {
unset($visiting[$key]); unset($visiting->keys[$key]);
} }
} }
$parent = $this->parents[$classKey] ?? null; $parent = $this->parents[$classKey] ?? null;

@ -459,10 +459,12 @@ trait SsaPropOptimizer
$rightProp = $this->getPropNameOfObj($node->expr, $objName); $rightProp = $this->getPropNameOfObj($node->expr, $objName);
if ($rightProp !== null) { if ($rightProp !== null) {
// $ref = &$o->prop changes the slot to a reference. Earlier // $ref = &$o->prop changes the zval slot itself from the
// optimized accesses remain safe only if the property is not // declared scalar into IS_REFERENCE. A C++ reference hoisted
// touched again afterward. // from any earlier access would then alias the zend_reference
$events[] = ['kind' => 'danger', 'prop' => $rightProp]; // 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); $this->collectPropEventsInDynamicParts($node->expr, $objName, $events);
} else { } else {
$this->collectPropEvents($node->expr, $objName, $events); $this->collectPropEvents($node->expr, $objName, $events);
@ -475,7 +477,9 @@ trait SsaPropOptimizer
&& $node->args[0] instanceof Node\Arg) { && $node->args[0] instanceof Node\Arg) {
$propName = $this->getPropNameOfObj($node->args[0]->value, $objName); $propName = $this->getPropNameOfObj($node->args[0]->value, $objName);
if ($propName !== null) { 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); $this->collectPropEventsInDynamicParts($node->args[0]->value, $objName, $events);
return; return;
} }

@ -64,7 +64,16 @@ trait ArrayExpressionTrait
$this->indentLevel++; $this->indentLevel++;
foreach ($items as $item) { foreach ($items as $item) {
$this->assertExprCanBeUsedAsValue($item->value, 'array value'); $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) { if ($item->key) {
$this->assertExprCanBeUsedAsValue($item->key, 'array key'); $this->assertExprCanBeUsedAsValue($item->key, 'array key');
$key = $this->parseArrayKey($item->key); $key = $this->parseArrayKey($item->key);
@ -290,7 +299,10 @@ trait ArrayExpressionTrait
} }
$value = $this->convertToRef($item->value); $value = $this->convertToRef($item->value);
} else { } else {
$value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); $value = $this->materializeRefReturnAsValue(
$item->value,
$this->parseOrderedOperand($item->value, false),
);
} }
if ($item->unpack) { if ($item->unpack) {
$this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');'; $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');';

@ -631,16 +631,25 @@ trait AssignOpTrait
$this->addLocalVar($var, Type::STD_ORDERED_MAP); $this->addLocalVar($var, Type::STD_ORDERED_MAP);
return $this->parseStdOrderedMap($var, $right); return $this->parseStdOrderedMap($var, $right);
} else { } else {
if (!$this->hasVar($var)) {
$valueExpr = $this->parseStdCall($right); $valueExpr = $this->parseStdCall($right);
$finalVarType = $right->getAttribute('nativeType') ?? Type::VAR; $finalVarType = $right->getAttribute('nativeType') ?? Type::VAR;
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $finalVarType); $this->addLocalVar($var, $finalVarType);
}
if ($finalVarType !== Type::VAR) { if ($finalVarType !== Type::VAR) {
$this->context->explicitNativeTypeVars[$var] = true; $this->context->explicitNativeTypeVars[$var] = true;
} }
return $var . ' = ' . $valueExpr; return $var . ' = ' . $valueExpr;
} }
// 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;
}
}
} }
} elseif ($this->isVarExpr($right)) { } elseif ($this->isVarExpr($right)) {
$rightVar = $this->parseIdentifier($right); $rightVar = $this->parseIdentifier($right);
@ -669,7 +678,12 @@ trait AssignOpTrait
$this->addLocalVar($var, $finalVarType); $this->addLocalVar($var, $finalVarType);
} else { } else {
$finalVarType = $this->getVarType($var); $finalVarType = $this->getVarType($var);
$rawVarType = $this->getRawVarType($var);
if (Type::isTypedRefType($rawVarType)) {
$this->checkTypedReferenceAssignExpr($left, $rawVarType, $type);
} else {
$this->checkVarAssignExpr($left, $finalVarType, $type); $this->checkVarAssignExpr($left, $finalVarType, $type);
}
$declaredObjectClass = $this->getDeclaredObjectType($var); $declaredObjectClass = $this->getDeclaredObjectType($var);
if (!$assigningNullToTypedObject if (!$assigningNullToTypedObject
&& $finalVarType === Type::OBJECT && $finalVarType === Type::OBJECT
@ -736,6 +750,20 @@ trait AssignOpTrait
: $rightExprType; : $rightExprType;
return $var . ' = ' . $this->convertNativePropertyWriteExpr($propertyDef->type, $effectiveRightType, $rightExpr); 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 $assignedExpr = $finalVarType === Type::VAR
? $rightExpr ? $rightExpr
: $this->convertExprType($rightExpr, $finalVarType, $rightExprType); : $this->convertExprType($rightExpr, $finalVarType, $rightExprType);
@ -893,7 +921,7 @@ trait AssignOpTrait
&& $this->isZeroLiteral($node->expr) && $this->isZeroLiteral($node->expr)
&& $this->isVarExpr($node->var) && $this->isVarExpr($node->var)
&& $this->hasVar((string) $this->parseIdentifier($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 // std::int()/std::float() values are an explicit opt-in to native
// C++ arithmetic; changing them to PHP semantics here would be as // 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->var, $expr, true);
$this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false); $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; $propertyReferenceTarget = null;
$nativeObjectProperty = $expr->var instanceof Expr\PropertyFetch $nativeObjectProperty = $expr->var instanceof Expr\PropertyFetch
&& $this->getNativePropertyClassDef($expr->var)?->nativeObject === true; && $this->getNativePropertyClassDef($expr->var)?->nativeObject === true;

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

@ -1080,6 +1080,12 @@ trait PropertyAccessTrait
if (!$this->hasVar($name)) { if (!$this->hasVar($name)) {
$this->errorUndefinedVariable($var); $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); $type = $this->getVarType($name);
if ($this->isNativeObjectVar($name)) { if ($this->isNativeObjectVar($name)) {
$this->forgetNativeObjectNonNull($name); $this->forgetNativeObjectNonNull($name);

@ -241,6 +241,7 @@ trait TypeConversionTrait
protected function convertExprFromType(string $type, string $expr): string protected function convertExprFromType(string $type, string $expr): string
{ {
$type = Type::getReferencedType($type);
if ($type === Type::FLOAT) { if ($type === Type::FLOAT) {
return $this->convertFloatExpr($expr); return $this->convertFloatExpr($expr);
} }
@ -295,10 +296,12 @@ trait TypeConversionTrait
} }
/** /**
* PHP references are untyped zval aliases. Exposing fixed C++ storage as * A reference created from an ordinary local has no Zend type source.
* one would let dynamic code replace the value with an incompatible type. * Exposing fixed C++ storage through such a reference would therefore let
* Only php::Var (or an existing php::Ref) has storage that can safely * dynamic code replace the value with an incompatible type. Typed object
* participate in Zend reference semantics. * 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( protected function assertVariableReferenceStorage(
NodeAbstract $expr, NodeAbstract $expr,

@ -1087,11 +1087,52 @@ class Preprocessor extends CompilerBase
// Record late-bound parameter type keywords so they can be re-resolved // 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. // to the consuming class when a trait method is flattened into a class.
$argInfo->typeKeyword = $typeKeyword; $argInfo->typeKeyword = $typeKeyword;
// Ordinary PHP references use php::Ref at the native ABI. Native if (!$param->byRef) {
// object references are rejected after the complete signature has return $type;
// been parsed: a typed pointer already shares object identity, while }
// PHP & would additionally expose caller-slot rebinding.
return $param->byRef ? Type::REF : $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) { if ($param->type === null || $param->type instanceof NullableType) {
$argInfo->nullable = true; $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 NullableType
|| $param->type instanceof UnionType || $param->type instanceof UnionType
|| $param->type instanceof IntersectionType || $param->type instanceof IntersectionType

@ -529,11 +529,27 @@ final class PythonToTypePhpConverter
private function deleteStatement(array $node): array private function deleteStatement(array $node): array
{ {
$targets = []; $targets = [];
$walk = function (array $target) use (&$walk, &$targets, $node): void { foreach ($node['targets'] ?? [] as $target) {
$this->collectDeleteTargets($target, $node, $targets);
}
$lines = [];
foreach ($targets as $target) {
$lines[] = $this->line('unset(' . $this->target($target) . ');');
}
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. // del (a, b) / del [a, b] expands element by element.
if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) { if (in_array($target['_type'] ?? '', ['Tuple', 'List'], true)) {
foreach ($target['elts'] ?? [] as $element) { foreach ($target['elts'] ?? [] as $element) {
$walk($element); $this->collectDeleteTargets($element, $node, $targets);
} }
return; return;
} }
@ -541,15 +557,6 @@ final class PythonToTypePhpConverter
$this->unsupported($node, 'unsupported del target'); $this->unsupported($node, 'unsupported del target');
} }
$targets[] = $target; $targets[] = $target;
};
foreach ($node['targets'] ?? [] as $target) {
$walk($target);
}
$lines = [];
foreach ($targets as $target) {
$lines[] = $this->line('unset(' . $this->target($target) . ');');
}
return $lines;
} }
/** @param array<string, mixed> $node */ /** @param array<string, mixed> $node */

@ -286,10 +286,9 @@ trait ClassConstantValueTrait
): mixed ): mixed
{ {
$lateBoundScope ??= $scope; $lateBoundScope ??= $scope;
$evaluator = null; $evaluator = new ConstExprEvaluator(function (Node\Expr $expr) use ($scope, $lateBoundScope): mixed {
$evaluator = new ConstExprEvaluator(function (Node\Expr $expr) use (&$evaluator, $scope, $lateBoundScope): mixed {
if ($expr instanceof Node\Expr\Cast) { if ($expr instanceof Node\Expr\Cast) {
$value = $evaluator->evaluateDirectly($expr->expr); $value = $this->evaluateCompileTimeExpression($expr->expr, $scope, $lateBoundScope);
return match (true) { return match (true) {
$expr instanceof Node\Expr\Cast\Int_ => (int) $value, $expr instanceof Node\Expr\Cast\Int_ => (int) $value,
$expr instanceof Node\Expr\Cast\Double => (float) $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) { if ($expr instanceof Node\Expr\ClassConstFetch && $expr->class instanceof Node\Name) {
$name = $expr->name instanceof Node\Identifier $name = $expr->name instanceof Node\Identifier
? $expr->name->toString() ? $expr->name->toString()
: $evaluator->evaluateDirectly($expr->name); : $this->evaluateCompileTimeExpression($expr->name, $scope, $lateBoundScope);
if (!is_string($name)) { if (!is_string($name)) {
throw new \RuntimeException('A compile-time class constant name must evaluate to string'); 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) { 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) { if ($object === null && $expr instanceof Node\Expr\NullsafePropertyFetch) {
return null; return null;
} }
@ -363,7 +362,7 @@ trait ClassConstantValueTrait
} }
$property = $expr->name instanceof Node\Identifier $property = $expr->name instanceof Node\Identifier
? $expr->name->toString() ? $expr->name->toString()
: $evaluator->evaluateDirectly($expr->name); : $this->evaluateCompileTimeExpression($expr->name, $scope, $lateBoundScope);
if ($property === 'name') { if ($property === 'name') {
return $object->caseName; return $object->caseName;
} }

@ -573,25 +573,27 @@ final class TestCoverageAnalyzer
/** @param Node[] $nodes @return list<Node\Stmt\ClassMethod> */ /** @param Node[] $nodes @return list<Node\Stmt\ClassMethod> */
private function findClassMethods(array $nodes): array private function findClassMethods(array $nodes): array
{ {
$methods = []; $result = new \stdClass();
$this->walkValues($nodes, static function (Node $node) use (&$methods): void { $result->values = [];
$this->walkValues($nodes, static function (Node $node) use ($result): void {
if ($node instanceof Node\Stmt\ClassMethod) { 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}> */ /** @param Node[] $nodes @return list<array{name: string, node: Expr\MethodCall}> */
private function collectMethodCalls(array $nodes): array private function collectMethodCalls(array $nodes): array
{ {
$calls = []; $result = new \stdClass();
$this->walkValues($nodes, static function (Node $node) use (&$calls): void { $result->values = [];
$this->walkValues($nodes, static function (Node $node) use ($result): void {
if ($node instanceof Expr\MethodCall && $node->name instanceof Node\Identifier) { 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 private function findPhpFixtureArgument(Expr\MethodCall $call): ?string
@ -609,8 +611,9 @@ final class TestCoverageAnalyzer
*/ */
private function collectProviderRows(Node\Stmt\ClassMethod $method): array private function collectProviderRows(Node\Stmt\ClassMethod $method): array
{ {
$rows = []; $result = new \stdClass();
$this->walkValues($method->stmts ?? [], static function (Node $node) use (&$rows): void { $result->values = [];
$this->walkValues($method->stmts ?? [], static function (Node $node) use ($result): void {
if (!$node instanceof Expr\Yield_ || !$node->value instanceof Expr\Array_) { if (!$node instanceof Expr\Yield_ || !$node->value instanceof Expr\Array_) {
return; return;
} }
@ -621,10 +624,12 @@ final class TestCoverageAnalyzer
} }
} }
if ($strings !== []) { if ($strings !== []) {
$rows[] = $strings; $result->values[] = $strings;
} }
}); });
$rows = $result->values;
if ($rows !== []) { if ($rows !== []) {
return $rows; return $rows;
} }
@ -686,11 +691,14 @@ final class TestCoverageAnalyzer
return $fixtures; return $fixtures;
} }
if ($value instanceof Node) { 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')) { 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)); return array_values(array_unique($fixtures));
} }
@ -808,8 +816,10 @@ final class TestCoverageAnalyzer
/** @param list<Node> $ancestors @param array<string, true> $features */ /** @param list<Node> $ancestors @param array<string, true> $features */
private function detectSemanticNodeFeatures(Node $node, array $ancestors, array &$features): void private function detectSemanticNodeFeatures(Node $node, array $ancestors, array &$features): void
{ {
$mark = static function (string $id) use (&$features): void { $result = new \stdClass();
$features['semantic:' . $id] = true; $result->features = $features;
$mark = static function (string $id) use ($result): void {
$result->features['semantic:' . $id] = true;
}; };
if ($node instanceof Node\Name\Relative) { if ($node instanceof Node\Name\Relative) {
@ -916,13 +926,16 @@ final class TestCoverageAnalyzer
} }
} }
} }
$features = $result->features;
} }
/** @param array<string, true> $features */ /** @param array<string, true> $features */
private function detectSourceFeatures(string $code, array &$features): void private function detectSourceFeatures(string $code, array &$features): void
{ {
$mark = static function (string $id) use (&$features): void { $result = new \stdClass();
$features['semantic:' . $id] = true; $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) 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) || 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)) { if (preg_match('/\b(?:exit|die)\s*\(\s*message\s*:/i', $code)) {
$mark('exit_named_argument'); $mark('exit_named_argument');
} }
$features = $result->features;
} }
private function unionContainsIntersection(Node\UnionType $type): bool private function unionContainsIntersection(Node\UnionType $type): bool

@ -4594,9 +4594,14 @@ CODE;
// separate pass so one trait method can call another method declared // separate pass so one trait method can call another method declared
// later in the same or a nested trait. // later in the same or a nested trait.
foreach ($composedTraitMethods as [$stmt, $origin]) { foreach ($composedTraitMethods as [$stmt, $origin]) {
$this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void { $traitMethodCodes = $this->withTraitNameContext($origin, function () use ($stmt): array {
$this->parseClassMethod($stmt, $methodCodes); $codes = [];
$this->parseClassMethod($stmt, $codes);
return $codes;
}); });
foreach ($traitMethodCodes as $methodName => $methodCode) {
$methodCodes[$methodName] = $methodCode;
}
} }
if (!$class instanceof Node\Stmt\Trait_) { if (!$class instanceof Node\Stmt\Trait_) {
$this->validateOverrideAttributes($class); $this->validateOverrideAttributes($class);
@ -4751,6 +4756,7 @@ CODE;
} }
$callParams = ''; $callParams = '';
$refWrapVars = [];
foreach ($functionDef->argInfoList as $k => $argInfo) { foreach ($functionDef->argInfoList as $k => $argInfo) {
$var = 'arg_' . $argInfo->name; $var = 'arg_' . $argInfo->name;
if ($argInfo->variadic) { if ($argInfo->variadic) {
@ -4791,7 +4797,18 @@ CODE;
} }
$cppType = $this->getDefaultArgumentType($argInfo); $cppType = $this->getDefaultArgumentType($argInfo);
$declaredClass = $argInfo->declaredClass ?: $argInfo->class; $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; $rawVar = 'raw_' . $var;
$cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = ' . $argExpr . ';' . PHP_EOL; $cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = ' . $argExpr . ';' . PHP_EOL;
$cppCode .= $this->genStrictScalarParamCheck( $cppCode .= $this->genStrictScalarParamCheck(
@ -4806,8 +4823,10 @@ CODE;
} else { } else {
$expr = $this->convertExprFromType($argInfo->type, $argExpr); $expr = $this->convertExprFromType($argInfo->type, $argExpr);
} }
if (!Type::isTypedRefType($argInfo->type)) {
$cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL; $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL;
} }
}
$callParam = $var; $callParam = $var;
if ($this->canConsumeForwardedArgument($argInfo)) { if ($this->canConsumeForwardedArgument($argInfo)) {
$callParam = 'php::takeValue(' . $var . ')'; $callParam = 'php::takeValue(' . $var . ')';
@ -4832,12 +4851,18 @@ CODE;
if ($functionDef->returnType !== Type::VOID) { if ($functionDef->returnType !== Type::VOID) {
$cppCode .= $this->getIndent() . 'auto retval = ' . $fn . '(' . $callParams . ');' . PHP_EOL; $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; $cppCode .= $this->getIndent() . 'php::move(retval, return_value);' . PHP_EOL;
if (!$functionDef->returnsByRef) { if (!$functionDef->returnsByRef) {
$cppCode .= $this->getIndent() . 'php::deref(return_value);' . PHP_EOL; $cppCode .= $this->getIndent() . 'php::deref(return_value);' . PHP_EOL;
} }
} else { } else {
$cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL; $cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL;
foreach ($refWrapVars as $refWrapVar) {
$cppCode .= $this->getIndent() . $refWrapVar . '.commit();' . PHP_EOL;
}
} }
$this->indentLevel--; $this->indentLevel--;
$cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL; $cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL;
@ -5833,7 +5858,9 @@ CODE;
} }
$declaredClass = $arg->declaredClass ?: $arg->class; $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::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']], Type::FLOAT => [['kind' => 'isFloat']],
Type::BOOL => [['kind' => 'isBool']], Type::BOOL => [['kind' => 'isBool']],

@ -24,5 +24,50 @@ final class Type
public const string ARGS = 'php::Args'; public const string ARGS = 'php::Args';
public const string STR = 'php::Str'; public const string STR = 'php::Str';
public const string REF = 'php::Ref'; 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 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); $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); return $this->convertToRef($arg->value);
} }

@ -2642,19 +2642,23 @@ class EvaluatedValue
$evaluationState = new \stdClass(); $evaluationState = new \stdClass();
$evaluationState->isUnknownConstValue = false; $evaluationState->isUnknownConstValue = false;
$evaluator = null; // The holder permits recursive evaluator access without taking a
$evaluator = new ConstExprEvaluator( // 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 ( static function (Expr $expr) use (
$allConstInfos, $allConstInfos,
$evaluationState, $evaluationState,
&$evaluator, $evaluator,
) { ) {
// php-parser's ConstExprEvaluator predates PHP 8.5 constant // php-parser's ConstExprEvaluator predates PHP 8.5 constant
// expression casts. Keep the compatibility logic in TypePHP: // expression casts. Keep the compatibility logic in TypePHP:
// validation has already rejected void and disallowed object // validation has already rejected void and disallowed object
// casts before declaration values reach gen_stub.php. // casts before declaration values reach gen_stub.php.
if ($expr instanceof Expr\Cast) { if ($expr instanceof Expr\Cast) {
$value = $evaluator->evaluateDirectly($expr->expr); $value = $evaluator->instance->evaluateDirectly($expr->expr);
return match (true) { return match (true) {
$expr instanceof Expr\Cast\Int_ => (int) $value, $expr instanceof Expr\Cast\Int_ => (int) $value,
$expr instanceof Expr\Cast\Double => (float) $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; $enumCaseRef = null;
if ($result instanceof \TypePhp\Entity\EnumCaseRef) { if ($result instanceof \TypePhp\Entity\EnumCaseRef) {
@ -6049,13 +6053,14 @@ function generateArgInfoCode(
$code .= "\n"; $code .= "\n";
} }
$generatedFuncInfos = []; $generatedFuncInfos = new \stdClass();
$generatedFuncInfos->values = [];
$argInfoCode = generateCodeWithConditions( $argInfoCode = generateCodeWithConditions(
$fileInfo->getAllFuncInfos(), "\n", $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 there already is an equivalent arginfo structure, only emit a #define */
if ($generatedFuncInfo = $funcInfo->findEquivalent($generatedFuncInfos)) { if ($generatedFuncInfo = $funcInfo->findEquivalent($generatedFuncInfos->values)) {
$code = sprintf( $code = sprintf(
"#define %s %s\n", "#define %s %s\n",
$funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName()
@ -6064,7 +6069,7 @@ function generateArgInfoCode(
$code = $funcInfo->toArgInfoCode($fileInfo->getMinimumPhpVersionIdCompatibility()); $code = $funcInfo->toArgInfoCode($fileInfo->getMinimumPhpVersionIdCompatibility());
} }
$generatedFuncInfos[] = $funcInfo; $generatedFuncInfos->values[] = $funcInfo;
return $code; return $code;
} }
); );
@ -6085,16 +6090,17 @@ function generateArgInfoCode(
$code .= "$framelessFunctionCode\n"; $code .= "$framelessFunctionCode\n";
} }
$generatedFunctionDeclarations = []; $generatedFunctionDeclarations = new \stdClass();
$generatedFunctionDeclarations->keys = [];
$code .= generateCodeWithConditions( $code .= generateCodeWithConditions(
$fileInfo->getAllFuncInfos(), "", $fileInfo->getAllFuncInfos(), "",
static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarations) { static function (FuncInfo $funcInfo) use ($fileInfo, $generatedFunctionDeclarations) {
$key = $funcInfo->getDeclarationKey(); $key = $funcInfo->getDeclarationKey();
if (isset($generatedFunctionDeclarations[$key])) { if (isset($generatedFunctionDeclarations->keys[$key])) {
return null; return null;
} }
$generatedFunctionDeclarations[$key] = true; $generatedFunctionDeclarations->keys[$key] = true;
return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); 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 * 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 { 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, $funcInfos,
"", "",
static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, &$declaredStrings) { static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, $declaredStringState) {
$code = null; $code = null;
if ($funcInfo->name instanceof MethodName) { if ($funcInfo->name instanceof MethodName) {
@ -6200,10 +6208,10 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all
// conditionally available; string reuse is only among declarations // conditionally available; string reuse is only among declarations
// that are always there // that are always there
if ($funcInfo->cond) { if ($funcInfo->cond) {
$empty = []; $useDeclared = new \stdClass();
$useDeclared = &$empty; $useDeclared->values = [];
} else { } else {
$useDeclared = &$declaredStrings; $useDeclared = $declaredStringState;
} }
foreach ($funcInfo->attributes as $key => $attribute) { foreach ($funcInfo->attributes as $key => $attribute) {
@ -6213,7 +6221,7 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all
"func_" . $funcInfo->name->getNameForAttributes() . "_$key", "func_" . $funcInfo->name->getNameForAttributes() . "_$key",
$allConstInfos, $allConstInfos,
$phpVersionIdMinimumCompatibility, $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", "func_{$funcInfo->name->getNameForAttributes()}_arg{$index}_$key",
$allConstInfos, $allConstInfos,
$phpVersionIdMinimumCompatibility, $phpVersionIdMinimumCompatibility,
\std::ref($useDeclared) \std::ref($useDeclared->values)
); );
} }
} }
@ -6234,6 +6242,13 @@ function generateFunctionAttributeInitialization(iterable $funcInfos, array $all
}, },
$parentCond $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) { if ($phpVersionIdMinimumCompatibility !== null && $phpVersionIdMinimumCompatibility < PHP_85_VERSION_ID) {
$isConditional = true; $isConditional = true;
} }
$declaredStringState = new \stdClass();
$declaredStringState->values = $declaredStrings;
$code = generateCodeWithConditions( $code = generateCodeWithConditions(
$constInfos, $constInfos,
"", "",
static function (ConstInfo $constInfo) use ($allConstInfos, $isConditional, &$declaredStrings) { static function (ConstInfo $constInfo) use ($allConstInfos, $isConditional, $declaredStringState) {
$code = ""; $code = "";
if ($constInfo->attributes === []) { if ($constInfo->attributes === []) {
@ -6266,10 +6283,10 @@ function generateGlobalConstantAttributeInitialization(
// conditionally available; string reuse is only among declarations // conditionally available; string reuse is only among declarations
// that are always there // that are always there
if ($constInfo->cond) { if ($constInfo->cond) {
$empty = []; $useDeclared = new \stdClass();
$useDeclared = &$empty; $useDeclared->values = [];
} else { } else {
$useDeclared = &$declaredStrings; $useDeclared = $declaredStringState;
} }
$constName = str_replace('\\', '\\\\', $constInfo->name->__toString()); $constName = str_replace('\\', '\\\\', $constInfo->name->__toString());
$constVarName = 'const_' . $constName; $constVarName = 'const_' . $constName;
@ -6281,7 +6298,7 @@ function generateGlobalConstantAttributeInitialization(
$constVarName . "_$key", $constVarName . "_$key",
$allConstInfos, $allConstInfos,
PHP_85_VERSION_ID, PHP_85_VERSION_ID,
\std::ref($useDeclared) \std::ref($useDeclared->values)
); );
} }
@ -6289,6 +6306,8 @@ function generateGlobalConstantAttributeInitialization(
}, },
$parentCond $parentCond
); );
$updatedDeclaredStrings = $declaredStringState->values;
$declaredStrings = $updatedDeclaredStrings;
if ($code && $isConditional) { if ($code && $isConditional) {
return "\n#if (PHP_VERSION_ID >= " . PHP_85_VERSION_ID . ")\n" . $code . "#endif\n"; return "\n#if (PHP_VERSION_ID >= " . PHP_85_VERSION_ID . ")\n" . $code . "#endif\n";
} }
@ -6308,20 +6327,22 @@ function generateConstantAttributeInitialization(
?string $parentCond = null, ?string $parentCond = null,
array &$declaredStrings = [] array &$declaredStrings = []
): string { ): string {
return generateCodeWithConditions( $declaredStringState = new \stdClass();
$declaredStringState->values = $declaredStrings;
$code = generateCodeWithConditions(
$constInfos, $constInfos,
"", "",
static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, &$declaredStrings) { static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility, $declaredStringState) {
$code = null; $code = null;
// Make sure we don't try and use strings that might only be // Make sure we don't try and use strings that might only be
// conditionally available; string reuse is only among declarations // conditionally available; string reuse is only among declarations
// that are always there // that are always there
if ($constInfo->cond) { if ($constInfo->cond) {
$empty = []; $useDeclared = new \stdClass();
$useDeclared = &$empty; $useDeclared->values = [];
} else { } else {
$useDeclared = &$declaredStrings; $useDeclared = $declaredStringState;
} }
foreach ($constInfo->attributes as $key => $attribute) { foreach ($constInfo->attributes as $key => $attribute) {
$code .= $attribute->generateCode( $code .= $attribute->generateCode(
@ -6329,7 +6350,7 @@ function generateConstantAttributeInitialization(
"const_" . $constInfo->name->getDeclarationName() . "_$key", "const_" . $constInfo->name->getDeclarationName() . "_$key",
$allConstInfos, $allConstInfos,
$phpVersionIdMinimumCompatibility, $phpVersionIdMinimumCompatibility,
\std::ref($useDeclared) \std::ref($useDeclared->values)
); );
} }
@ -6337,6 +6358,9 @@ function generateConstantAttributeInitialization(
}, },
$parentCond $parentCond
); );
$updatedDeclaredStrings = $declaredStringState->values;
$declaredStrings = $updatedDeclaredStrings;
return $code;
} }
/** /**
@ -7161,22 +7185,24 @@ function generateStubFile(string $stubFile, string $objectFile, bool $forceRegen
} }
} }
array_map( $argCount = max(count($aliasArgs), count($aliasedArgs));
function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { for ($argIndex = 0; $argIndex < $argCount; $argIndex++) {
$aliasArg = $aliasArgs[$argIndex] ?? null;
$aliasedArg = $aliasedArgs[$argIndex] ?? null;
if ($aliasArg === null) { if ($aliasArg === null) {
assert($aliasedArg !== null); assert($aliasedArg !== null);
$errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing";
return null; continue;
} }
if ($aliasedArg === null) { if ($aliasedArg === null) {
$errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing";
return null; continue;
} }
if ($aliasArg->name !== $aliasedArg->name) { 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"; $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name";
return null; continue;
} }
if ($aliasArg->type != $aliasedArg->type) { if ($aliasArg->type != $aliasedArg->type) {
@ -7186,9 +7212,7 @@ function generateStubFile(string $stubFile, string $objectFile, bool $forceRegen
if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { 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"; $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value";
} }
}, }
$aliasArgs, $aliasedArgs
);
$aliasedReturn = $aliasedFunc->return; $aliasedReturn = $aliasedFunc->return;
$aliasReturn = $aliasFunc->return; $aliasReturn = $aliasFunc->return;

@ -140,33 +140,33 @@ function main()
Assert::true($array->isEmpty()); Assert::true($array->isEmpty());
$fruits1 = array("lemon", "orange", "banana", "apple"); $fruits1 = array("lemon", "orange", "banana", "apple");
$fruits2 = std::any(array("lemon", "orange", "banana", "apple")); $fruits2 = array("lemon", "orange", "banana", "apple");
sort($fruits2); sort($fruits2);
Assert::eq($fruits1->sort(), $fruits2); Assert::eq($fruits1->sort(), $fruits2);
Assert::eq($fruits1, $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($stack->pop(), 'raspberry');
Assert::eq(array_pop($stack), 'apple'); Assert::eq(array_pop($stack), 'apple');
$mutableArray = std::any(array("red","green")); $mutableArray = array("red","green");
$mutableArray->push("blue"); $mutableArray->push("blue");
Assert::eq($mutableArray, ["red","green", "blue"]); Assert::eq($mutableArray, ["red","green", "blue"]);
array_push($mutableArray, "yellow"); array_push($mutableArray, "yellow");
Assert::eq($mutableArray, ["red","green", "blue", "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($stack->shift(), 'orange');
Assert::eq(array_shift($stack), 'banana'); Assert::eq(array_shift($stack), 'banana');
$queue = std::any(["orange", "banana"]); $queue = ["orange", "banana"];
$queue->unshift("orange"); $queue->unshift("orange");
Assert::eq($queue, ["orange", "orange", "banana"]); Assert::eq($queue, ["orange", "orange", "banana"]);
array_unshift($queue, "orange"); array_unshift($queue, "orange");
Assert::eq($queue, ["orange", "orange", "orange", "banana"]); Assert::eq($queue, ["orange", "orange", "orange", "banana"]);
$array1 = array("red", "green", "blue", "yellow"); $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->splice(2), array_splice($spliceArray, 2));
Assert::eq($array1, $spliceArray); Assert::eq($array1, $spliceArray);

@ -14,11 +14,6 @@ function sumImmutable(#[Immutable] array $values): int
return count($values) + $values->count() + $values[0] + $values[1]; return count($values) + $values->count() + $values[0] + $values[1];
} }
function inspectImmutableReference(#[Immutable] ImmutableUser &$user): string
{
return $user->name();
}
trait ImmutableNameTrait trait ImmutableNameTrait
{ {
#[Immutable] #[Immutable]
@ -118,8 +113,6 @@ function main(): void
echo $user->traitName(), PHP_EOL; echo $user->traitName(), PHP_EOL;
echo sumImmutable([2, 3]), PHP_EOL; echo sumImmutable([2, 3]), PHP_EOL;
echo cloneImmutable($user), PHP_EOL; echo cloneImmutable($user), PHP_EOL;
$referenceUser = std::any($user);
echo inspectImmutableReference($referenceUser), PHP_EOL;
echo deliberatelyEscapeImmutableCheck($user), PHP_EOL; echo deliberatelyEscapeImmutableCheck($user), PHP_EOL;
echo (new ImmutableHookedValue())->read(), PHP_EOL; echo (new ImmutableHookedValue())->read(), PHP_EOL;
echo closureImmutableParameter($user), PHP_EOL; echo closureImmutableParameter($user), PHP_EOL;
@ -131,7 +124,6 @@ Rango:Rango
Rango Rango
9 9
Clone Clone
Rango
Dynamic Dynamic
HOOK HOOK
Dynamic Dynamic

@ -2,6 +2,8 @@
pow int overflow pow int overflow
--FILE-- --FILE--
<?php <?php
use varint_types;
function main() function main()
{ {
$a = 2 ** 80; $a = 2 ** 80;

@ -5,7 +5,7 @@ unset on arrays and variables
function main() { function main() {
// Unset variable // Unset variable
$a = 10; $a = std::any(10);
unset($a); unset($a);
var_dump(isset($a)); var_dump(isset($a));

@ -55,7 +55,7 @@ function main()
var_dump($k); var_dump($k);
// Test 7: normal variable unset + ?? // Test 7: normal variable unset + ??
$m = 50; $m = std::any(50);
unset($m); unset($m);
var_dump($m ?? 777); var_dump($m ?? 777);

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

@ -2,6 +2,8 @@
Magic Methods - __get, __set, __call, __invoke etc. Magic Methods - __get, __set, __call, __invoke etc.
--FILE-- --FILE--
<?php <?php
use varint_types;
class Point { class Point {
public function __construct( public function __construct(
private float $x = 0.0, private float $x = 0.0,

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

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

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

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

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

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

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

@ -1,13 +1,15 @@
--TEST-- --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-- --FILE--
<?php <?php
use varint_types;
function phpDiv(int $a, int $b): mixed { return $a / $b; } function phpDiv(int $a, int $b): mixed { return $a / $b; }
function phpMod(int $a, int $b): mixed { return $a % $b; } function phpMod(int $a, int $b): mixed { return $a % $b; }
function main(): void 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(phpDiv(7, 2));
var_dump(phpMod(-7, 2)); var_dump(phpMod(-7, 2));

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

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

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

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

@ -2,6 +2,8 @@
SSA: int SSA: int
--FILE-- --FILE--
<?php <?php
use varint_types;
function main(): void { function main(): void {
ini_set('precision', 17); ini_set('precision', 17);
$a = 100; $a = 100;

@ -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 function main(): void
{ {
$fixedValues = ['item' => 'fixed-before'];
$fixedItem =& array_element_ref($fixedValues);
$fixedItem = 'fixed-after';
var_dump($fixedValues['item']);
$values = std::any([ $values = std::any([
'item' => 'before', 'item' => 'before',
'outer' => ['inner' => 'nested-before'], 'outer' => ['inner' => 'nested-before'],
@ -45,6 +50,7 @@ function main(): void
} }
?> ?>
--EXPECT-- --EXPECT--
string(11) "fixed-after"
string(5) "after" string(5) "after"
string(12) "nested-after" string(12) "nested-after"
string(12) "object-after" string(12) "object-after"

@ -12,7 +12,7 @@ final class PrivatePropertyReferenceHolder
{ {
private ?array $value = null; private ?array $value = null;
public function bind(array &$source): void public function bind(mixed &$source): void
{ {
$this->value = &$source; $this->value = &$source;
} }

@ -1,5 +1,11 @@
--TEST-- --TEST--
ref call arg ref call arg
--SKIPIF--
<?php
if (!class_exists('ZipArchive')) {
die('skip zip extension is required');
}
?>
--FILE-- --FILE--
<?php <?php
function main() function main()
@ -14,8 +20,10 @@ function main()
$zip->close(); $zip->close();
if ($zip->open($path) === TRUE) { if ($zip->open($path) === TRUE) {
$opsys = std::any(null);
$attr = std::any(null);
for ($idx = 0; $s = $zip->statIndex($idx); $idx++) { 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); var_dump($rs, $idx, $opsys, $attr);
} }
$zip->close(); $zip->close();

@ -1,5 +1,5 @@
--TEST-- --TEST--
Reference parameter with object type - property access and method call Mixed reference parameters can replace objects while preserving object operations
--FILE-- --FILE--
<?php <?php
class Test 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->value);
var_dump($test->abc()); 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]=> [1]=>
NULL NULL
[2]=> [2]=>
&string(9) "reference" string(9) "reference"
} }
string(9) "reference" string(9) "reference"
string(11) "child-write" string(11) "child-write"

@ -37,6 +37,6 @@ float(100)
bool(true) bool(true)
int(99) int(99)
float(2026) float(2026)
float(2.5) int(2)
int(2) int(2)
float(10) 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 function convert_values(string $suffix, &...$vars): bool
{ {
$ok = std::any(true); $ok = std::any(true);
foreach ($vars as &$var) { $convert = static function (&$value, $key) use (&$ok, $suffix): void {
array_walk_recursive($var, static function (&$value, $key) use (&$ok, $suffix): void {
if (!is_string($value)) { if (!is_string($value)) {
$ok = false; $ok = false;
return; return;
} }
$value .= $suffix . ':' . $key; $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); unset($var);
return $ok; return $ok;

@ -26,7 +26,7 @@ function normalize(int|string &...$values): void
unset($value); unset($value);
} }
function bump_objects(Counter &...$values): void function bump_objects(mixed &...$values): void
{ {
foreach ($values as $value) { foreach ($values as $value) {
$value->value++; $value->value++;

Loading…
Cancel
Save