feat(parser): add strong PHP-array contract support with typedArray feature

- Add typedArray property to ArgInfo entity for strong array contracts
- Remove deprecated ArrayDef functionality and related test files
- Implement typed array access validation and assignment handling
- Add typed array support in closure generation and variable binding
- Introduce compilation error checks for typed array references
- Add comprehensive typed array attribute and property tests
- Update call argument generator to validate typed array arguments
- Implement typed array compound write restrictions
- Add typed array alias and rebind safety checks
master
韩天峰 3 weeks ago
parent 8da7b5b4e5
commit f3f686ec46
  1. 4
      README-CN.md
  2. 4
      README.md
  3. 42
      docs/en/ARRAY_DEF.md
  4. 6
      docs/en/COMPILE_TIME_FUNCTIONS.md
  5. 2
      docs/en/README.md
  6. 36
      docs/en/TYPED_ARRAYS.md
  7. 6
      docs/zh-cn/COMPILE_TIME_FUNCTIONS.md
  8. 2
      docs/zh-cn/README.md
  9. 24
      docs/zh-cn/TYPED_ARRAYS.md
  10. 7
      phpunit/code/array-def-class-map-key.php
  11. 13
      phpunit/code/array-def-inclusive-upper-bound.php
  12. 6
      phpunit/code/array-def-invalid-map-key.php
  13. 10
      phpunit/code/array-def-map-append.php
  14. 8
      phpunit/code/array-def-native-class-value.php
  15. 6
      phpunit/code/array-def-no-arguments.php
  16. 6
      phpunit/code/array-def-non-array-property.php
  17. 12
      phpunit/code/array-def-static-class-mismatch.php
  18. 10
      phpunit/code/array-def-static-key-mismatch.php
  19. 11
      phpunit/code/array-def-static-value-mismatch.php
  20. 11
      phpunit/code/array-def-std-container-value.php
  21. 6
      phpunit/code/array-def-too-many-arguments.php
  22. 7
      phpunit/code/typed-array-property-class-map-key.php
  23. 13
      phpunit/code/typed-array-property-inclusive-upper-bound.php
  24. 6
      phpunit/code/typed-array-property-invalid-map-key.php
  25. 10
      phpunit/code/typed-array-property-map-append.php
  26. 8
      phpunit/code/typed-array-property-native-class-value.php
  27. 6
      phpunit/code/typed-array-property-no-arguments.php
  28. 6
      phpunit/code/typed-array-property-non-array-property.php
  29. 12
      phpunit/code/typed-array-property-static-class-mismatch.php
  30. 10
      phpunit/code/typed-array-property-static-key-mismatch.php
  31. 11
      phpunit/code/typed-array-property-static-value-mismatch.php
  32. 11
      phpunit/code/typed-array-property-std-container-value.php
  33. 6
      phpunit/code/typed-array-property-too-many-arguments.php
  34. 39
      phpunit/src/ArrayDefTest.php
  35. 4
      phpunit/src/CompileTimeAttributeRegistryTest.php
  36. 113
      phpunit/src/StdAttributeTypeTest.php
  37. 39
      phpunit/src/TypedArrayPropertyTest.php
  38. 169
      phpunit/src/TypedArrayTest.php
  39. 255
      src/ArrayDef/ArrayDefSupportTrait.php
  40. 19
      src/ArrayDef/ArrayDefWritePlan.php
  41. 23
      src/ArrayDef/ArrayDefinition.php
  42. 24
      src/CompilerBase.php
  43. 3
      src/Context/CompilationStateTrait.php
  44. 2
      src/Context/FunctionContext.php
  45. 2
      src/Entity/ArgInfo.php
  46. 4
      src/Entity/PropertyDef.php
  47. 13
      src/Entity/TypedArrayPropertyWritePlan.php
  48. 5
      src/Generator/CallArgumentGenerator.php
  49. 19
      src/Generator/ClosureGenerator.php
  50. 2
      src/Optimizer/FuncCallOptimizer.php
  51. 6
      src/Parser/ArrayExpressionTrait.php
  52. 58
      src/Parser/AssignOpTrait.php
  53. 4
      src/Parser/ForeachTrait.php
  54. 8
      src/Parser/FunctionCallTrait.php
  55. 2
      src/Parser/PropertyAccessTrait.php
  56. 28
      src/Parser/StdContainerTrait.php
  57. 1
      src/Parser/TypeConversionTrait.php
  58. 419
      src/Parser/TypedArrayTrait.php
  59. 21
      src/Parser/UniversalMethodCall.php
  60. 28
      src/Preprocessor.php
  61. 2
      src/Transform/CompileTimeAttribute.php
  62. 6
      src/Transform/CompileTimeAttributeRegistry.php
  63. 30
      src/Translator.php
  64. 37
      src/polyfills.php
  65. 84
      tests/compiler/array-def/compound-types.phpt
  66. 71
      tests/compiler/typed-array/attributes-and-properties.phpt
  67. 57
      tests/compiler/typed-array/dynamic-keys.phpt
  68. 46
      tests/compiler/typed-array/exact-and-readonly.phpt
  69. 55
      tests/compiler/typed-array/objects.phpt
  70. 22
      tests/compiler/typed-array/property-classes.phpt
  71. 29
      tests/compiler/typed-array/property-list-holes.phpt
  72. 32
      tests/compiler/typed-array/property-php-values.phpt
  73. 55
      tests/compiler/typed-array/property-zend-and-native.phpt
  74. 62
      tests/compiler/typed-array/storage.phpt

@ -81,7 +81,7 @@ AST,待全部项目符号就绪后再在 convert 阶段解析。这一两阶
- **编译期函数与关键词** —— `std::any()`、`std::object()`、`std::ref()`、
`std::expected()`、`std::unexpected()`,以及 `toObject()`、`toInt()`、
`toString()`、`toArray()` 等。
- **编译期安全检查** —— `#[Immutable]` 只读契约和 `#[ArrayDef]` 数组结构元数据,
- **编译期安全检查** —— `#[Immutable]` 只读契约和 `StdList` / `StdDict` 数组类型注解,
在编译期检查,零运行时开销。
- **编译期代码生成** —— `#[Getter]`、`#[Setter]`、`#[With]`、`#[Constructor]`、
`#[Printer]` 和 `#[Arrayable]` 根据属性声明生成类型安全的方法。
@ -736,7 +736,7 @@ GitHub Actions 会在 PHP 8.4 和 8.5 上分别运行 PHPUnit 与自举 PHPT。
- [编译期函数](docs/zh-cn/COMPILE_TIME_FUNCTIONS.md) —— `std::any()`、`std::ref()`、`std::expected()` 等
- [混合 C++/PHP](docs/zh-cn/MIXED_CPP_PHP.md) —— C++/PHP 互操作
- [`#[Immutable]`](docs/zh-cn/IMMUTABLE.md) —— 编译期只读契约
- [`#[ArrayDef]`](docs/zh-cn/ARRAY_DEF.md) —— 强类型数组属性契约
- [强类型 PHP 数组与类型注解](docs/zh-cn/TYPED_ARRAYS.md) —— 强类型数组属性契约
- [Property hooks](docs/zh-cn/PROPERTY_HOOKS.md) —— PHP 8.4 hook 降级和运行时元数据
- [对象存储模型](docs/zh-cn/OBJECT_STORAGE_AND_PASSING_MODELS.md) —— Zend object、Box 与 Native class 边界
- [Generator](docs/zh-cn/YIELD_GENERATOR.md) —— 生成器降级与生命周期

@ -93,7 +93,7 @@ This two-phase design keeps multi-file and self-hosted builds deterministic.
- **Compile-time functions & keywords** — `std::any()`, `std::object()`,
`std::ref()`, `std::expected()`, `std::unexpected()`, plus `toObject()`,
`toInt()`, `toString()`, `toArray()` and friends.
- **Compile-time safety** — `#[Immutable]` read-only contracts and `#[ArrayDef]`
- **Compile-time safety** — `#[Immutable]` read-only contracts and `StdList` / `StdDict`
array-shape metadata, checked at compile time with zero runtime cost.
- **Compile-time code generation** — `#[Getter]`, `#[Setter]`, `#[With]`,
`#[Constructor]`, `#[Printer]`, and `#[Arrayable]` generate type-safe methods
@ -795,7 +795,7 @@ rules and a PHPT whenever runtime output or diagnostics are observable.
- [Compile-time functions](docs/en/COMPILE_TIME_FUNCTIONS.md) — `std::any()`, `std::ref()`, `std::expected()`, …
- [Mixed C++/PHP](docs/en/MIXED_CPP_PHP.md) — C++/PHP interop
- [`#[Immutable]`](docs/en/IMMUTABLE.md) — compile-time read-only contracts
- [`#[ArrayDef]`](docs/en/ARRAY_DEF.md) — typed array-property contracts
- [Typed PHP arrays and type annotations](docs/en/TYPED_ARRAYS.md) — typed array-property contracts
- [Property hooks](docs/en/PROPERTY_HOOKS.md) — PHP 8.4 hook lowering and runtime metadata
- [Object storage models](docs/en/OBJECT_STORAGE_AND_PASSING_MODELS.md) — Zend object, Box, and Native class boundaries
- [Generators](docs/en/YIELD_GENERATOR.md) — generator lowering and lifecycle

@ -1,42 +0,0 @@
# `#[ArrayDef]` compile-time array contracts
`#[ArrayDef]` attaches key/value type information to a property declared
exactly as `array`. It supports both Zend classes and `#[Native]` classes and
has no runtime metadata or per-read overhead.
```php
class Index
{
#[ArrayDef(Type::String)]
public array $names = []; // list<string>
#[ArrayDef(Type::Int, Type::String)]
public array $labels = []; // map<int, string>
}
```
One argument defines a list value type. Two arguments define a map key type
and value type. Map keys are restricted to `Type::Int` or `Type::String`.
`ClassName::class` is therefore valid only as a list element type or as the
second (value) argument of a map.
For direct writes whose expression types are known, the compiler either emits
the normal write unchanged or reports a fatal type error. An `any` key/value is
checked with PHPX exact-type helpers at runtime. No coercive `intval()` or
string conversion is performed.
List writes support `[]` and non-negative integer indexes up to PHP's current
append position. Indexed writes uniformly emit `php::safeArrayIndex(index,
array)`. The helper uses `zend_hash_next_free_element()` and follows the
initial-index rule of `zend_hash_next_index_insert()`, which remains correct
when `unset()` has created holes or removed the highest numeric key. An index
equal to that value behaves like the next `$array[]` append; earlier indexes
may update or refill an element. Negative indexes and indexes beyond the append
position fail at runtime. There is no AST special case for
`property[count(property)]`. Maps do not support `[]` append writes.
The contract intentionally applies only to direct element assignment lowered
by TypePHP. Reads and in-place operators are unchanged. Values passed through
dynamic functions, callbacks, Reflection, `eval()`, or other ZendVM escape
paths are outside the contract and have undefined behavior from ArrayDef's
perspective.

@ -41,7 +41,7 @@ Constraints:
## `std::` compile-time entry points
There are currently 15 `std::` compile-time entry points.
There are currently 17 `std::` compile-time entry points.
| Name | Purpose | Main limitation |
| --- | --- | --- |
@ -60,6 +60,10 @@ There are currently 15 `std::` compile-time entry points.
| `std::vector($type[, $size])` | Constructs a std vector. | Can only be used in the top-level scope of the variable's first assignment. |
| `std::map($keyType, $valueType)` | Constructs a std map. | Can only be used in the top-level scope of the variable's first assignment. |
| `std::orderedMap($keyType, $valueType)` | Constructs a std ordered map. | Can only be used in the top-level scope of the variable's first assignment. |
| `std::list($valueType)` | Creates an integer-key typed PHP array with negative/sparse indices and append. | First assignment of a new function-local variable; strict dynamic keys, no dynamic reference mutation. |
| `std::dict($keyType, $valueType)` | Creates a typed PHP dictionary with `Type::Int` or `Type::Str` keys. | First assignment of a new function-local variable; explicit keys required, no append. |
Lists and dicts retain PHP array storage and copy-on-write without `toStd*()` recovery. Values require matching static types, while dynamic `any` / `var` keys receive internal strict checks. Only read-only dynamic PHP array calls are allowed, without `std::ref()`. Parameters need exactly matching `StdList` / `StdDict` type annotations, with the PHP type omitted or declared as `array`, never `mixed`; matching native `&` parameters can modify the caller's array. See [Typed PHP Arrays and Type Annotations](TYPED_ARRAYS.md) for examples and boundary details.
## Std container conversion keyword methods

@ -15,7 +15,7 @@ This directory contains compiler implementation, compatibility, build-mode, and
- [Three Object Storage and Passing Models](OBJECT_STORAGE_AND_PASSING_MODELS.md): the responsibilities, ABI, and non-substitutable boundaries of Zend Object, PHPX Box, and Native Class Object.
- [Universal and Extension Methods](UNIVERSAL_METHODS.md), [Generator](YIELD_GENERATOR.md).
- [`#[Immutable]` compile-time read-only contract](IMMUTABLE.md): methods, parameters, aliases, call boundaries, and dynamic escape rules.
- [`#[ArrayDef]` array property contract](ARRAY_DEF.md): List/Map metadata, direct-write checks, and dynamic escape boundaries.
- [Typed PHP arrays and type annotations](TYPED_ARRAYS.md): List/Map metadata, direct-write checks, and dynamic escape boundaries.
- [Class Inheritance](CLASS_INHERITANCE.md), [Mixed C++/PHP](MIXED_CPP_PHP.md).
## Architecture and maintenance

@ -0,0 +1,36 @@
# Typed PHP arrays and type annotations
`std::list(T)` and `std::dict(K, V)` retain PHP array storage and copy-on-write.
Type annotations declare their key and value contracts on parameters and properties:
```php
class State
{
#[StdList(Type::Int)] public array $values = [];
#[StdDict(Type::Str, Type::Int)] public $counts = [];
}
function append(#[StdList(Type::Int)] array &$values): void
{
$values[] = 42;
}
```
PHP types may be omitted or declared as compatible storage types: `array` for
`StdList` / `StdDict`, and `box` for `StdVector` / `StdMap` / `StdOrderedMap`.
Explicit `mixed`, `any`, nullable types, unions, and incompatible types are rejected.
List keys are integers, including negative and sparse keys; no bounds checks
are inserted. Only lists allow `[]` append. Dicts require an explicit int or
string key. Dynamic `any` / `var` keys get internal strict type checks, not coercion.
Values require matching static types, except for `Type::Any` values.
Local typed arrays cannot escape to dynamic PHP through `std::ref()`, element
references, or mutable/by-reference array functions. Matching annotated native
parameters may accept references. String-key dict iteration converts numeric
PHP keys back to strings without changing PHPX or HashTable storage.
Property annotations currently check first-level direct element assignments.
They do not provide complete protection against whole-property replacement,
dynamic PHP object mutation, or object/property reference escapes. Property reads
are not automatically promoted to closed-contract local typed arrays.

@ -38,7 +38,7 @@ TypePHP 不再为编译器指令保留任何全局函数名。编译期 API 最
## `std::` 编译期入口
当前 `std::` 编译期入口共 15 个。
当前 `std::` 编译期入口共 17 个。
| 名称 | 作用 | 主要限制 |
| --- | --- | --- |
@ -57,6 +57,10 @@ TypePHP 不再为编译器指令保留任何全局函数名。编译期 API 最
| `std::vector($type[, $size])` | 构造 std vector。 | 只能在变量首次赋值的顶层作用域使用。 |
| `std::map($keyType, $valueType)` | 构造 std map。 | 只能在变量首次赋值的顶层作用域使用。 |
| `std::orderedMap($keyType, $valueType)` | 构造 std ordered map。 | 只能在变量首次赋值的顶层作用域使用。 |
| `std::list($valueType)` | 创建整数键强类型 PHP 数组,支持负数、稀疏索引和追加。 | 新函数局部变量首次赋值;动态键严格检查,禁止动态引用修改。 |
| `std::dict($keyType, $valueType)` | 创建强类型 PHP 字典,键为 `Type::Int` 或 `Type::Str`。 | 新函数局部变量首次赋值;必须显式提供键,不支持追加。 |
list/dict 保留普通 PHP 数组存储和写时复制,不需要 `toStd*()`。值要求静态类型匹配,`any` / `var` 键插入内部严格检查;仅允许只读动态 PHP 数组调用,禁止 `std::ref()`。参数使用完全一致的 `StdList` / `StdDict` 类型注解,PHP 类型可省略或为 `array`,不允许 `mixed`;同类型的原生 `&` 参数可以修改调用方。详细示例及边界见[强类型 PHP 数组与类型注解](TYPED_ARRAYS.md)。
## Std 容器转换关键词方法

@ -15,7 +15,7 @@
- [三套对象存储与传递模型](OBJECT_STORAGE_AND_PASSING_MODELS.md):Zend Object、PHPX Box 与 Native Class Object 的职责、ABI 和不可替代边界。
- [通用与扩展方法](UNIVERSAL_METHODS.md)、[Generator](YIELD_GENERATOR.md)。
- [`#[Immutable]` 编译期只读契约](IMMUTABLE.md):方法、参数、别名、调用边界与动态逃逸规则。
- [`#[ArrayDef]` 数组属性契约](ARRAY_DEF.md):List/Map 元数据、直接写入检查与动态逃逸边界。
- [强类型 PHP 数组与类型注解](TYPED_ARRAYS.md):List/Map 元数据、直接写入检查与动态逃逸边界。
- [类继承](CLASS_INHERITANCE.md)、[混合 C++/PHP](MIXED_CPP_PHP.md)。
## 架构与维护

@ -0,0 +1,24 @@
# 强类型 PHP 数组与类型注解
`std::list(T)` / `std::dict(K, V)` 保留普通 PHP 数组存储与写时复制,使用类型注解声明参数或属性的键和值类型:
```php
class State
{
#[StdList(Type::Int)] public array $values = [];
#[StdDict(Type::Str, Type::Int)] public $counts = [];
}
function append(#[StdList(Type::Int)] array &$values): void
{
$values[] = 42;
}
```
PHP 类型可以省略或声明为兼容类型:`StdList` / `StdDict` 对应 `array`,`StdVector` / `StdMap` / `StdOrderedMap` 对应 `box`。不允许显式 `mixed`、`any`、可空类型、联合类型和其他不兼容类型。
list 支持负数、稀疏整数键和空洞,不做边界检查;只有 list 允许 `[]` 追加。dict 必须显式提供 int 或 str 键。动态 `any` / `var` 键插入内部严格检查,不做隐式转换。值要求静态类型匹配,`Type::Any` 值除外。
局部强类型数组禁止通过 `std::ref()`、元素引用、可修改或引用传递的数组函数逃逸到动态 PHP。类型一致的原生参数可以按引用传递。字符串键 dict 遍历时将 PHP 数字键恢复为字符串,不修改 phpx 或底层 HashTable。
当前属性类型注解检查第一层直接元素赋值,并不完整保护整属性替换、动态 PHP 对象修改或对象/属性引用逃逸。属性读取不会自动升级为受封闭契约保护的局部强类型数组。

@ -1,7 +0,0 @@
<?php
class ArrayDefClassMapKeyType {}
class ArrayDefClassMapKeyBox
{
#[ArrayDef(ArrayDefClassMapKeyType::class, Type::String)]
public array $value = [];
}

@ -1,13 +0,0 @@
<?php
class ArrayDefInclusiveUpperBound
{
#[ArrayDef(Type::String)]
public array $values = [];
}
function writeArrayDefInclusiveUpperBound(ArrayDefInclusiveUpperBound $box, int $index): void
{
$box->values[$index] = 'indexed';
$box->values[count($box->values)] = 'counted';
}

@ -1,6 +0,0 @@
<?php
class ArrayDefInvalidMapKey
{
#[ArrayDef(Type::Bool, Type::String)]
public array $value = [];
}

@ -1,10 +0,0 @@
<?php
class ArrayDefMapAppend
{
#[ArrayDef(Type::Int, Type::String)]
public array $value = [];
}
function arrayDefMapAppend(ArrayDefMapAppend $box): void
{
$box->value[] = 'bad';
}

@ -1,8 +0,0 @@
<?php
#[Native]
class ArrayDefNativeValue {}
class ArrayDefNativeValueBox
{
#[ArrayDef(ArrayDefNativeValue::class)]
public array $values = [];
}

@ -1,6 +0,0 @@
<?php
class ArrayDefNoArguments
{
#[ArrayDef]
public array $value = [];
}

@ -1,6 +0,0 @@
<?php
class ArrayDefNonArrayProperty
{
#[ArrayDef(Type::Int)]
public string $value = '';
}

@ -1,12 +0,0 @@
<?php
class ArrayDefExpectedUser {}
class ArrayDefOtherUser {}
class ArrayDefClassMismatchBox
{
#[ArrayDef(ArrayDefExpectedUser::class)]
public array $users = [];
}
function arrayDefStaticClassMismatch(ArrayDefClassMismatchBox $box): void
{
$box->users[] = new ArrayDefOtherUser();
}

@ -1,10 +0,0 @@
<?php
class ArrayDefStaticKeyMismatch
{
#[ArrayDef(Type::Int, Type::String)]
public array $value = [];
}
function arrayDefStaticKeyMismatch(ArrayDefStaticKeyMismatch $box): void
{
$box->value['bad'] = 'value';
}

@ -1,11 +0,0 @@
<?php
#[Native]
class ArrayDefStaticValueMismatch
{
#[ArrayDef(Type::String)]
public array $value = [];
}
function arrayDefStaticValueMismatch(ArrayDefStaticValueMismatch $box): void
{
$box->value[] = 123;
}

@ -1,11 +0,0 @@
<?php
class ArrayDefStdContainerValueBox
{
#[ArrayDef(Type::Any)]
public array $values = [];
}
function arrayDefStdContainerValue(ArrayDefStdContainerValueBox $box): void
{
$values = std::vector(Type::Int);
$box->values[] = $values;
}

@ -1,6 +0,0 @@
<?php
class ArrayDefTooManyArguments
{
#[ArrayDef(Type::Int, Type::String, Type::Bool)]
public array $value = [];
}

@ -0,0 +1,7 @@
<?php
class TypedArrayPropertyClassMapKeyType {}
class TypedArrayPropertyClassMapKeyBox
{
#[StdDict(TypedArrayPropertyClassMapKeyType::class, Type::String)]
public array $value = [];
}

@ -0,0 +1,13 @@
<?php
class TypedArrayPropertyInclusiveUpperBound
{
#[StdList(Type::String)]
public array $values = [];
}
function writeTypedArrayPropertyInclusiveUpperBound(TypedArrayPropertyInclusiveUpperBound $box, int $index): void
{
$box->values[$index] = 'indexed';
$box->values[count($box->values)] = 'counted';
}

@ -0,0 +1,6 @@
<?php
class TypedArrayPropertyInvalidMapKey
{
#[StdDict(Type::Bool, Type::String)]
public array $value = [];
}

@ -0,0 +1,10 @@
<?php
class TypedArrayPropertyMapAppend
{
#[StdDict(Type::Int, Type::String)]
public array $value = [];
}
function typedArrayPropertyMapAppend(TypedArrayPropertyMapAppend $box): void
{
$box->value[] = 'bad';
}

@ -0,0 +1,8 @@
<?php
#[Native]
class TypedArrayPropertyNativeValue {}
class TypedArrayPropertyNativeValueBox
{
#[StdList(TypedArrayPropertyNativeValue::class)]
public array $values = [];
}

@ -0,0 +1,6 @@
<?php
class TypedArrayPropertyNoArguments
{
#[StdList]
public array $value = [];
}

@ -0,0 +1,6 @@
<?php
class TypedArrayPropertyNonArrayProperty
{
#[StdList(Type::Int)]
public string $value = '';
}

@ -0,0 +1,12 @@
<?php
class TypedArrayPropertyExpectedUser {}
class TypedArrayPropertyOtherUser {}
class TypedArrayPropertyClassMismatchBox
{
#[StdList(TypedArrayPropertyExpectedUser::class)]
public array $users = [];
}
function typedArrayPropertyStaticClassMismatch(TypedArrayPropertyClassMismatchBox $box): void
{
$box->users[] = new TypedArrayPropertyOtherUser();
}

@ -0,0 +1,10 @@
<?php
class TypedArrayPropertyStaticKeyMismatch
{
#[StdDict(Type::Int, Type::String)]
public array $value = [];
}
function typedArrayPropertyStaticKeyMismatch(TypedArrayPropertyStaticKeyMismatch $box): void
{
$box->value['bad'] = 'value';
}

@ -0,0 +1,11 @@
<?php
#[Native]
class TypedArrayPropertyStaticValueMismatch
{
#[StdList(Type::String)]
public array $value = [];
}
function typedArrayPropertyStaticValueMismatch(TypedArrayPropertyStaticValueMismatch $box): void
{
$box->value[] = 123;
}

@ -0,0 +1,11 @@
<?php
class TypedArrayPropertyStdContainerValueBox
{
#[StdList(Type::Int)]
public array $values = [];
}
function typedArrayPropertyStdContainerValue(TypedArrayPropertyStdContainerValueBox $box): void
{
$values = std::vector(Type::Int);
$box->values[] = $values;
}

@ -0,0 +1,6 @@
<?php
class TypedArrayPropertyTooManyArguments
{
#[StdDict(Type::Int, Type::String, Type::Bool)]
public array $value = [];
}

@ -1,39 +0,0 @@
<?php
use TypePhp\CompilerTest;
final class ArrayDefTest extends \BaseTest
{
public function testArrayDefDeclarationAndDirectWriteDiagnostics(): void
{
$this->exec('ArrayDef can only be applied to properties declared as array', 'array-def-non-array-property.php');
$this->exec('ArrayDef expects one or two type arguments', 'array-def-no-arguments.php');
$this->exec('ArrayDef expects one or two type arguments', 'array-def-too-many-arguments.php');
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-invalid-map-key.php');
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-class-map-key.php');
$this->exec('ArrayDef map properties do not support append writes', 'array-def-map-append.php');
$this->exec('expects key of type int, string given', 'array-def-static-key-mismatch.php');
$this->exec('expects value of type string, int given', 'array-def-static-value-mismatch.php');
$this->exec('expects value of type ArrayDefExpectedUser, ArrayDefOtherUser given', 'array-def-static-class-mismatch.php');
$this->exec('Native class types cannot be used in ArrayDef', 'array-def-native-class-value.php');
$this->exec('Std Container values cannot be stored in ArrayDef properties', 'array-def-std-container-value.php');
}
public function testListIndexUsesPhpAppendBoundaryWithoutAstSpecialCase(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/array-def-inclusive-upper-bound.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertSame(2, substr_count($code, 'php::safeArrayIndex('));
self::assertStringNotContainsString('.length() + 1', $code);
self::assertStringNotContainsString('.newItem()', $code);
}
}

@ -10,7 +10,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
$expected = [
'Native', 'MethodsFor', 'NoExport', 'WasmExport', 'Getter', 'Setter', 'With', 'Printer', 'Arrayable',
'NotNull', 'NotEmpty', 'Validate', 'Override', 'MustUse', 'Immutable', 'Hot', 'Cold', 'Constructor',
'ArrayDef', 'StdVector', 'StdMap', 'StdOrderedMap',
'StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict',
];
$this->assertSame($expected, CompileTimeAttributeRegistry::names());
@ -27,7 +27,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
$this->assertContains('Getter', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Override', CompileTimeAttributeRegistry::names(true));
$this->assertSame(
['Native', 'MethodsFor', 'NoExport', 'WasmExport', 'ArrayDef', 'StdVector', 'StdMap', 'StdOrderedMap'],
['Native', 'MethodsFor', 'NoExport', 'WasmExport', 'StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict'],
CompileTimeAttributeRegistry::namesForPhase(CompileTimeAttributeRegistry::PHASE_PREPROCESS),
);
$this->assertSame(

@ -0,0 +1,113 @@
<?php
use PHPUnit\Framework\Attributes\DataProvider;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
use TypePhp\Type;
final class StdAttributeTypeTest extends BaseTest
{
private function translate(string $source): array
{
global $translator;
$directory = sys_get_temp_dir() . '/std-attribute-type-' . bin2hex(random_bytes(6));
mkdir($directory);
$file = $directory . '/declarations.php';
file_put_contents($file, '<?php ' . $source);
try {
$translator = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator->addFiles([$file]);
$translator->prepareFile($file);
return [file_get_contents($translator->convertFile($file)), $translator];
} finally {
unlink($file);
rmdir($directory);
}
}
public function testMatchingParameterStorageTypes(): void
{
[$code, $compiler] = $this->translate(<<<'PHP'
function vector_arg(#[StdVector(Type::Int)] box $v): void { $v[] = 1; }
function map_arg(#[StdMap(Type::Str, Type::Int)] box $v): void { $v['a'] = 2; }
function ordered_arg(#[StdOrderedMap(Type::Int, Type::Str)] box $v): void { $v[0] = 'a'; }
function list_arg(#[StdList(Type::Int)] array $v): void { $v[] = 3; }
function dict_arg(#[StdDict(Type::Str, Type::Int)] array &$v): void { $v['a'] = 4; }
PHP);
self::assertStringContainsString('php_vector_arg(php::Var v)', $code);
self::assertStringContainsString('php_list_arg(php::Array v)', $code);
self::assertStringContainsString('php_dict_arg(php::Array & v)', $code);
$getFunction = new ReflectionMethod($compiler, 'getFunction');
self::assertSame(Type::VAR, $getFunction->invoke($compiler, 'vector_arg')->argInfoList[0]->type);
self::assertSame(Type::ARRAY, $getFunction->invoke($compiler, 'list_arg')->argInfoList[0]->type);
self::assertSame(Type::ARRAY_REF, $getFunction->invoke($compiler, 'dict_arg')->argInfoList[0]->type);
}
public function testPropertyAnnotationsAndSparseWrites(): void
{
[$code, $compiler] = $this->translate(<<<'PHP'
class User { public int $id = 1; }
class State {
#[StdList(Type::Int)] public array $values = [];
#[StdDict(Type::Str, User::class)] public array $users = [];
#[StdList(Type::Str)] public $inferred = [];
#[StdDict(Type::Int, Type::Str)] public static array $labels = [];
#[StdVector(Type::Int)] public box $vector;
#[StdMap(Type::Str, Type::Int)] public box $map;
#[StdOrderedMap(Type::Int, User::class)] public $ordered;
}
#[Native] class NativeState { #[StdList(Type::Int)] public $values = []; }
function write_values(State $s, NativeState $n, $key): void {
$s->values[-2] = 10;
$s->values[100] = 20;
$s->values[] = 30;
$s->values[$key] = 40;
$s->users['123'] = new User();
$s->inferred[100] = 'sparse';
State::$labels[-3] = 'negative';
$n->values[-5] = 7;
}
PHP);
self::assertStringNotContainsString('safeArrayIndex(', $code);
self::assertStringContainsString('php::toIntExact(', $code);
$class = (new ReflectionMethod($compiler, 'getClass'))->invoke($compiler, 'State');
self::assertSame(Type::ARRAY, $class->getProperty('inferred')->type);
self::assertSame('list', $class->getProperty('values')->typedArray['kind']);
self::assertSame('dict', $class->getProperty('users')->typedArray['kind']);
self::assertSame('User', $class->getProperty('users')->typedArray['class']);
self::assertSame('vector', $class->getProperty('vector')->stdContainer['kind']);
self::assertSame('ordered_map', $class->getProperty('ordered')->stdContainer['kind']);
self::assertSame(Type::BOX, $class->getProperty('ordered')->type);
}
#[DataProvider('conflictingDeclarations')]
public function testConflictingDeclarations(string $source, string $message): void
{
try {
$this->translate($source);
self::fail('Conflicting container declaration was accepted');
} catch (TestError|\TypePhp\Exception\SyntaxError $error) {
self::assertStringContainsString($message, $error->getMessage());
}
}
public static function conflictingDeclarations(): iterable
{
foreach (['StdVector(Type::Int)', 'StdMap(Type::Str, Type::Int)', 'StdOrderedMap(Type::Int, Type::Str)'] as $attribute) {
foreach (['array', 'mixed', 'any', '?box', 'box|int'] as $type) {
yield $attribute . ' parameter ' . $type => ["function f(#[{$attribute}] {$type} \$v): void {}", 'unless it is box'];
yield $attribute . ' property ' . $type => ["class A { #[{$attribute}] public {$type} \$v; }", 'unless it is box'];
}
}
foreach (['StdList(Type::Int)', 'StdDict(Type::Str, Type::Int)'] as $attribute) {
foreach (['box', 'mixed', 'any', '?array', 'array|int'] as $type) {
yield $attribute . ' parameter ' . $type => ["function f(#[{$attribute}] {$type} \$v): void {}", 'unless it is array'];
yield $attribute . ' property ' . $type => ["class A { #[{$attribute}] public {$type} \$v; }", 'unless it is array'];
}
}
yield 'property container conflict' => ['class A { #[StdVector(Type::Int), StdList(Type::Int)] public $v; }', 'cannot be applied to the same declaration'];
yield 'property wrong list key' => ['class A { #[StdList(Type::Int)] public array $v = []; } function f(A $a): void { $a->v["1"] = 1; }', 'Typed array key must have type'];
yield 'property wrong list value' => ['class A { #[StdList(Type::Int)] public array $v = []; } function f(A $a): void { $a->v[] = "1"; }', 'Typed array value must have type'];
yield 'property dict append' => ['class A { #[StdDict(Type::Int, Type::Int)] public array $v = []; } function f(A $a): void { $a->v[] = 1; }', 'StdDict properties do not support append'];
}
}

@ -0,0 +1,39 @@
<?php
use TypePhp\CompilerTest;
final class TypedArrayPropertyTest extends \BaseTest
{
public function testPropertyDeclarationAndDirectWriteDiagnostics(): void
{
$this->exec('unless it is array', 'typed-array-property-non-array-property.php');
$this->exec('expects 1 type argument', 'typed-array-property-no-arguments.php');
$this->exec('expects 2 type argument', 'typed-array-property-too-many-arguments.php');
$this->exec('key only supports Type::Int or Type::String', 'typed-array-property-invalid-map-key.php');
$this->exec('key only supports Type::Int or Type::String', 'typed-array-property-class-map-key.php');
$this->exec('StdDict properties do not support append writes', 'typed-array-property-map-append.php');
$this->exec('Typed array key must have type', 'typed-array-property-static-key-mismatch.php');
$this->exec('Typed array value must have type', 'typed-array-property-static-value-mismatch.php');
$this->exec('Typed array value must be an instance of TypedArrayPropertyExpectedUser', 'typed-array-property-static-class-mismatch.php');
$this->exec('Typed PHP arrays cannot hold Native objects', 'typed-array-property-native-class-value.php');
$this->exec('Typed array value must have type', 'typed-array-property-std-container-value.php');
}
public function testListIndexAllowsSparseAndNegativeKeysWithoutBoundsChecks(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/typed-array-property-inclusive-upper-bound.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertStringNotContainsString('php::safeArrayIndex(', $code);
self::assertStringNotContainsString('.length() + 1', $code);
self::assertStringNotContainsString('.newItem()', $code);
}
}

@ -0,0 +1,169 @@
<?php
use PHPUnit\Framework\Attributes\DataProvider;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
final class TypedArrayTest extends BaseTest
{
private function translate(string $source, bool $varIntTypes = false): array
{
global $translator;
$directory = sys_get_temp_dir() . '/typephp-typed-array-' . bin2hex(random_bytes(6));
mkdir($directory);
$file = $directory . '/typed.php';
file_put_contents($file, '<?php ' . ($varIntTypes ? 'use varint_types; ' : '')
. str_replace('function main(', 'function probe(', $source));
try {
$translator = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator->addFiles([$file]);
$translator->prepareFile($file);
$generated = $translator->convertFile($file);
return [file_get_contents($generated), $translator];
} finally {
unlink($file);
rmdir($directory);
}
}
public function testStoragePropagationAndNativeReferences(): void
{
[$code, $compiler] = $this->translate(<<<'PHP'
function copy_values(#[StdList(Type::Int)] $values): void { $values[] = 99; }
function append_values(#[StdList(Type::Int)] &$values): void { $values[] = 42; }
class User { public int $id = 1; }
class Receiver {
public function accept(#[StdDict(Type::Str, User::class)] &$users): void { $users['alice'] = new User(); }
}
function main(): void {
$values = std::list(Type::Int);
$values[] = 7;
$copy = $values;
$alias =& $values;
$alias[] = 8;
copy_values($copy);
append_values(values: $alias);
var_dump(array_search(7, $values), array_keys($values), count($copy));
$users = std::dict(Type::Str, User::class);
$receiver = new Receiver();
$receiver->accept($users);
$user = $users['alice'];
var_dump($user->id);
}
PHP);
self::assertStringContainsString('php::Array &', $code);
self::assertStringContainsString('php::Array{}', $code);
self::assertStringNotContainsString('StdContainerBox', $code);
self::assertStringNotContainsString('toIntExact', $code);
self::assertStringNotContainsString('toObjectExact', $code);
self::assertStringNotContainsString('ZEND_FUNCTION(copy_values)', $code);
self::assertTrue($compiler->isNativeFunctionForStub('copy_values'));
self::assertTrue($compiler->isNativeMethodForStub('Receiver', 'accept'));
$function = (new ReflectionMethod($compiler, 'getFunction'))->invoke($compiler, 'append_values');
$parameter = $function->argInfoList[0];
self::assertSame(\TypePhp\Type::ARRAY_REF, $parameter->type);
self::assertSame($parameter->typedArray, unserialize(serialize($parameter))->typedArray);
}
public function testWideningIntegerExpressionRequiresExplicitConversion(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('may widen require an explicit');
$this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = $value + 1; }', true);
}
public function testExplicitNativeIntegerConversionInVarIntMode(): void
{
[$code] = $this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = std::int($value + 1); }', true);
self::assertStringContainsString('.appendValue(', $code);
}
public function testExistingIntegerKeywordConversionInVarIntMode(): void
{
[$code] = $this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = ($value + 1)->toInt(); }', true);
self::assertStringContainsString('php::toInt(', $code);
self::assertStringNotContainsString('toIntExact(', $code);
}
public function testDynamicKeysUseInternalStrictChecks(): void
{
[$code] = $this->translate(<<<'PHP'
function foo($key): void {
$list = std::list(Type::Int);
$list[$key] = 1;
var_dump($list[$key], isset($list[$key]), array_key_exists($key, $list));
unset($list[$key]);
$dict = std::dict(Type::Str, Type::Int);
$dict[$key] = 2;
var_dump($dict[$key], $dict->keyExists($key), $dict->get($key));
unset($dict[$key]);
}
PHP);
self::assertStringContainsString('php::toIntExact(', $code);
self::assertStringContainsString('php::toStringExact(', $code);
self::assertStringNotContainsString('toExactInt(', $code);
}
#[DataProvider('invalidDeclarations')]
public function testInvalidContractsAndEscapes(string $code, string $message): void
{
try {
$this->translate($code);
self::fail('Expected compilation failure');
} catch (TestError|\TypePhp\Exception\SyntaxError $error) {
self::assertStringContainsString($message, $error->getMessage());
}
}
public static function invalidDeclarations(): iterable
{
yield 'wrong value' => ['function main(): void { $a = std::list(Type::Int); $a[] = "x"; }', 'must have type'];
yield 'dynamic value' => ['function main($value): void { $a = std::list(Type::Int); $a[] = $value; }', 'must have type'];
yield 'wrong key' => ['function main(): void { $a = std::dict(Type::Int, Type::Str); $a["x"] = "v"; }', 'must have type'];
yield 'list string key' => ['function main(): void { $a = std::list(Type::Int); $a["x"] = 1; }', 'must have type'];
yield 'string dict append' => ['function main(): void { $a = std::dict(Type::Str, Type::Int); $a[] = 1; }', 'Only typed lists'];
yield 'integer dict append' => ['function main(): void { $a = std::dict(Type::Int, Type::Int); $a[] = 1; }', 'Only typed lists'];
yield 'missing list type' => ['function main(): void { $a = std::list(); }', 'expects 1 type'];
yield 'invalid dict key' => ['function main(): void { $a = std::dict(Type::Bool, Type::Int); }', 'key only supports'];
yield 'annotation plus mixed' => ['function foo(#[StdList(Type::Int)] mixed $a): void {}', 'a PHP type cannot also'];
yield 'duplicate annotation' => ['function foo(#[StdList(Type::Int), StdList(Type::Int)] $a): void {}', 'cannot be repeated'];
yield 'conflicting annotation' => ['function foo(#[StdList(Type::Int), StdVector(Type::Int)] $a): void {}', 'cannot be applied'];
yield 'named type argument' => ['function foo(#[StdList(valueType: Type::Int)] $a): void {}', 'positional type'];
yield 'wrong argument type' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = std::list(Type::Float); foo($a); }', 'identical list/dict contract'];
yield 'list and dict remain distinct' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = std::dict(Type::Int, Type::Int); foo($a); }', 'identical list/dict contract'];
yield 'untyped array argument' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = []; foo($a); }', 'identical list/dict contract'];
yield 'unannotated function' => ['function foo(array $a): void {} function main(): void { $a = std::list(Type::Int); foo($a); }', 'matching annotated'];
yield 'unannotated reference' => ['function foo(array &$a): void {} function main(): void { $a = std::list(Type::Int); foo($a); }', 'matching annotated'];
yield 'array push' => ['function main(): void { $a = std::list(Type::Int); array_push($a, 1); }', 'matching annotated'];
yield 'array sort' => ['function main(): void { $a = std::list(Type::Int); sort($a); }', 'matching annotated'];
yield 'array walk' => ['function main(): void { $a = std::list(Type::Int); array_walk($a, "var_dump"); }', 'matching annotated'];
yield 'dynamic ref wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $callback(std::ref($a)); }', 'cannot escape'];
yield 'dynamic element wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $a[] = 1; $callback(std::ref($a[0])); }', 'cannot escape'];
yield 'dynamic toRef wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $callback($a->toRef()); }', 'cannot escape'];
yield 'element reference' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $v =& $a[0]; }', 'cannot escape'];
yield 'reference return' => ['function &foo(#[StdList(Type::Int)] &$a) { return $a; }', 'cannot escape'];
yield 'element ref assignment' => ['function main(): void { $a = std::list(Type::Int); $v = 1; $a[] =& $v; }', 'cannot escape'];
yield 'property reference escape' => ['class A { public array $values = []; } function main(): void { $a = std::list(Type::Int); $object = new A(); $object->values =& $a; }', 'cannot escape'];
yield 'wrong copied write' => ['function main(): void { $a = std::list(Type::Int); $b = $a; $b[] = "x"; }', 'must have type'];
yield 'wrong alias write' => ['function main(): void { $a = std::list(Type::Int); $b =& $a; $b[] = "x"; }', 'must have type'];
yield 'plain array replacement' => ['function main(): void { $a = std::list(Type::Int); $a = []; }', 'identical list/dict contract'];
yield 'dynamic array replacement' => ['function main($other): void { $a = std::list(Type::Int); $a = $other; }', 'identical list/dict contract'];
yield 'incompatible object' => ['class A {} class B {} function main(): void { $a = std::list(A::class); $a[] = new B(); }', 'must be an instance'];
yield 'unknown object' => ['class A {} function main(object $value): void { $a = std::list(A::class); $a[] = $value; }', 'must be an instance'];
yield 'native object' => ['#[Native] class A {} function main(): void { $a = std::list(A::class); }', 'cannot hold Native'];
yield 'closure contract' => ['$fn = function(#[StdList(Type::Int)] $a) {};', 'named function or method'];
yield 'arrow contract' => ['$fn = fn(#[StdList(Type::Int)] $a) => 1;', 'named function or method'];
yield 'reference closure capture' => ['function main(): void { $a = std::list(Type::Int); $fn = function() use (&$a) {}; }', 'cannot be captured by reference'];
yield 'typed value closure capture' => ['function main(): void { $a = std::list(Type::Int); $fn = function() use ($a) { $a[] = "x"; }; $fn(); }', 'must have type'];
yield 'foreach ref' => ['function main(): void { $a = std::list(Type::Int); foreach ($a as &$v) {} }', 'cannot escape'];
yield 'incompatible override' => ['class A { public function foo(#[StdList(Type::Int)] $a): void {} } class B extends A { public function foo(#[StdList(Type::Float)] $a): void {} }', 'must be compatible'];
yield 'array reference escape' => ['function main(): void { $a = std::list(Type::Int); $bundle = [&$a]; }', 'cannot escape'];
yield 'compound write' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $a[0] += "x"; }', 'compound writes'];
yield 'coalescing write' => ['function main(): void { $a = std::list(Type::Int); $a[0] ??= "x"; }', 'compound writes'];
yield 'union write' => ['function main(): void { $a = std::list(Type::Int); $a += ["x"]; }', 'compound writes'];
yield 'increment write' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $a[0]++; }', 'increment/decrement'];
yield 'universal mutator' => ['function main(): void { $a = std::list(Type::Int); $a->set(0, "x"); }', 'untyped mutating'];
yield 'foreach value type' => ['function main(): void { $a = std::list(Type::Int); foreach ($a as $value) { $value = "x"; } }', 'Cannot re-assign'];
yield 'foreach key type' => ['function main(): void { $a = std::dict(Type::Str, Type::Int); foreach ($a as $key => $value) { $key = 1; } }', 'Cannot re-assign'];
}
}

@ -1,255 +0,0 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\ArrayDef;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
use TypePhp\Entity\PropertyDef;
use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Type;
/**
* Compile-time-only ArrayDef declaration parsing and direct-write checking.
*
* This deliberately does not attempt to guard array values that escape into
* ZendVM calls. ArrayDef is a static contract for syntax the compiler owns.
*/
trait ArrayDefSupportTrait
{
protected function parseArrayDefinition(Node\Stmt\Property|Node\Param $property): ?ArrayDefinition
{
$attribute = CompileTimeAttribute::find($property, 'ArrayDef');
if ($attribute === null) {
return null;
}
if (!$property->type instanceof Node\Identifier
|| strtolower($property->type->toString()) !== 'array'
) {
$this->fatalError($property, 'ArrayDef can only be applied to properties declared as array');
}
$count = count($attribute->args);
if ($count < 1 || $count > 2) {
$this->fatalError($attribute, 'ArrayDef expects one or two type arguments');
}
$types = [];
foreach ($attribute->args as $arg) {
if ($arg->name !== null || !$arg->value instanceof Expr\ClassConstFetch) {
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants');
}
$class = $arg->value->class;
$constant = $arg->value->name;
if (!$class instanceof Node\Name || !$constant instanceof Node\Identifier) {
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants');
}
$resolved = $class->getAttribute('resolvedName') ?? $class;
$resolvedClass = ltrim($resolved->toString(), '\\');
if (strcasecmp($constant->toString(), 'class') === 0) {
if ($this->isNativeObjectClass($resolvedClass)) {
$this->fatalError($arg, 'Native class types cannot be used in ArrayDef');
}
$types[] = $resolvedClass;
continue;
}
if (strcasecmp($resolvedClass, 'Type') !== 0) {
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants');
}
$types[] = $this->resolveArrayDefType($constant->toString(), $arg);
}
if ($count === 1) {
return new ArrayDefinition(null, $types[0]);
}
if (!in_array($types[0], [Type::INT, Type::STR], true)) {
$this->fatalError($attribute, 'ArrayDef map keys must use Type::Int or Type::String');
}
return new ArrayDefinition($types[0], $types[1]);
}
private function resolveArrayDefType(string $name, NodeAbstract $errorNode): string
{
$type = match (strtolower($name)) {
'int' => Type::INT,
'float' => Type::FLOAT,
'bool' => Type::BOOL,
'string' => Type::STR,
'array' => Type::ARRAY,
'object' => Type::OBJECT,
'any' => Type::VAR,
'stream' => Type::STREAM,
'bigint' => Type::BIGINT,
'bigfloat' => Type::BIGFLOAT,
'decimal' => Type::DECIMAL,
default => null,
};
if ($type === null) {
$this->fatalError($errorNode, "Unsupported ArrayDef type Type::{$name}");
}
return $type;
}
protected function prepareArrayDefDirectWrite(
Expr\ArrayDimFetch $left,
Expr $right,
string $value,
): ?ArrayDefWritePlan
{
$def = $this->getNativePropertyDef($left->var);
$arrayDef = $def?->arrayDef;
if ($arrayDef === null) {
return null;
}
$value = $this->validateArrayDefWriteValue($left->var, $right, $value, $arrayDef->valueType, 'value');
if ($left->dim === null) {
if (!$arrayDef->isList()) {
$this->fatalError($left, 'ArrayDef map properties do not support append writes');
}
return new ArrayDefWritePlan(true, null, $value);
}
$expectedKey = $arrayDef->keyType ?? Type::INT;
$key = $this->parseExprAsValue($left->dim);
$key = $this->validateArrayDefWriteValue($left->var, $left->dim, $key, $expectedKey, 'key');
if ($arrayDef->isList()) {
$array = $this->parseWritableIdentifier($left->var);
// PHP's append index does not shrink after unset(). Element count
// is therefore not a valid list-write boundary for sparse arrays.
$key = 'php::safeArrayIndex(' . $key . ', ' . $array . ')';
}
return new ArrayDefWritePlan(false, $key, $value);
}
private function validateArrayDefWriteValue(
NodeAbstract $property,
Expr $expr,
string $code,
string $expected,
string $part,
): string
{
$actual = $this->detectTypeOfExpr($expr);
$stdContainerValue = $this->isVarExpr($expr)
&& $this->isStdContainer($this->parseIdentifier($expr));
if ($part === 'value' && ($stdContainerValue || $this->isStdContainerType($actual))) {
$this->fatalError($expr, 'Std Container values cannot be stored in ArrayDef properties');
}
if ($expected === Type::VAR) {
return $code;
}
if ($this->isArrayDefClassType($expected)) {
$actualClass = $this->detectClassOfExpr($expr);
if ($actualClass !== '') {
if (!$this->isObjectClassStaticallyAssignableTo($actualClass, $expected)) {
$this->fatalError(
$expr,
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property)
. ' expects ' . $part . ' of type ' . $expected
. ', ' . $actualClass . ' given',
);
}
return $code;
}
if ($actual !== Type::VAR && $actual !== Type::OBJECT) {
$this->fatalError(
$expr,
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property)
. ' expects ' . $part . ' of type ' . $expected
. ', ' . $this->arrayDefTypeName($actual) . ' given',
);
}
return 'php::toObjectExact('
. $code . ', ' . $this->getClassEntryPtr($expected) . ', '
. $this->genCharPtr(
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part,
true,
)
. ')';
}
if ($actual !== Type::VAR) {
if ($actual !== $expected) {
$this->fatalError(
$expr,
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property)
. ' expects ' . $part . ' of type ' . $this->arrayDefTypeName($expected)
. ', ' . $this->arrayDefTypeName($actual) . ' given',
);
}
return $code;
}
$display = $this->genCharPtr(
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part,
true,
);
$helper = match ($expected) {
Type::INT => 'php::toIntExact',
Type::FLOAT => 'php::toFloatExact',
Type::BOOL => 'php::toBoolExact',
Type::STR => 'php::toStringExact',
Type::ARRAY => 'php::toArrayExact',
Type::OBJECT => 'php::toObjectExact',
Type::STREAM => 'php::toStreamExact',
Type::BIGINT => 'php::toBoxExact<php::BigInt>',
Type::BIGFLOAT => 'php::toBoxExact<php::BigFloat>',
Type::DECIMAL => 'php::toBoxExact<php::Decimal>',
default => null,
};
if ($helper === null) {
return $code;
}
$boxType = in_array($expected, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)
? ', ' . $this->genCharPtr($this->arrayDefTypeName($expected), true)
: '';
return $helper . '(' . $code . ', ' . $display . $boxType . ')';
}
private function arrayDefTypeName(string $type): string
{
return match ($type) {
Type::INT => 'int',
Type::FLOAT => 'float',
Type::BOOL => 'bool',
Type::STR => 'string',
Type::ARRAY => 'array',
Type::OBJECT => 'object',
Type::STREAM => 'stream',
Type::BIGINT => 'BigInt',
Type::BIGFLOAT => 'BigFloat',
Type::DECIMAL => 'Decimal',
Type::VAR => 'any',
default => $type,
};
}
private function isArrayDefClassType(string $type): bool
{
return !in_array($type, [
Type::INT,
Type::FLOAT,
Type::BOOL,
Type::STR,
Type::ARRAY,
Type::OBJECT,
Type::VAR,
Type::STREAM,
Type::BIGINT,
Type::BIGFLOAT,
Type::DECIMAL,
], true);
}
}

@ -1,19 +0,0 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\ArrayDef;
final readonly class ArrayDefWritePlan
{
public function __construct(
public bool $append,
public ?string $key,
public string $value,
) {
}
}

@ -1,23 +0,0 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\ArrayDef;
final readonly class ArrayDefinition
{
public function __construct(
public ?string $keyType,
public string $valueType,
) {
}
public function isList(): bool
{
return $this->keyType === null;
}
}

@ -92,7 +92,6 @@ use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
use TypePhp\NativeClass\NativeClassSupportTrait;
use TypePhp\NativeClass\NativeGlobalTypeResolver;
use TypePhp\Immutable\ImmutableSupportTrait;
use TypePhp\ArrayDef\ArrayDefSupportTrait;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\ArrayItem;
@ -115,7 +114,7 @@ class CompilerBase implements PropertyAccessContext
use NativeTypeCompatibilityTrait;
use NativeClassSupportTrait;
use ImmutableSupportTrait;
use ArrayDefSupportTrait;
use \TypePhp\Parser\TypedArrayTrait;
use NativeBuildConfigurationTrait;
use PythonModuleTrait;
use DeclarationSymbolTrait;
@ -2362,6 +2361,9 @@ class CompilerBase implements PropertyAccessContext
protected function detectClassOfExpr(NodeAbstract $expr): string
{
if (($typedArray = $this->getTypedArrayAccessDefinition($expr)) !== null) {
return $typedArray['class'] ?? '';
}
// Error suppression changes diagnostics only; it must never erase the
// static type of the wrapped expression. This is especially important
// for Native objects because treating their typed pointer as php::Var
@ -2720,6 +2722,7 @@ class CompilerBase implements PropertyAccessContext
if ($v->expr === null) {
return 'return ' . Type::REF . '{};';
}
$this->assertTypedArrayReferenceForbidden($v->expr);
if ($v->expr instanceof CallLike) {
$returnsByRef = $this->resolveRefReturningCall($v->expr);
if ($returnsByRef !== false) {
@ -3618,6 +3621,9 @@ class CompilerBase implements PropertyAccessContext
}
break;
case 'Expr_ArrayDimFetch':
if (($typedArray = $this->getTypedArrayAccessDefinition($expr)) !== null) {
return $typedArray['type'];
}
if ($this->isStdArrayExpr($expr)) {
if (!$expr->hasAttribute('stdArrayDimFetch')) {
$this->parseStdArrayDimFetch($expr);
@ -3709,6 +3715,9 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreInc(Expr\PreInc $expr): string
{
if ($this->getTypedArrayAccessDefinition($expr->var) !== null) {
$this->fatalError($expr, 'Typed array increment/decrement requires an explicit checked element assignment');
}
$this->assertImmutableMutationTarget($expr->var);
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '++');
@ -4120,6 +4129,9 @@ class CompilerBase implements PropertyAccessContext
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
{
if ($this->getTypedArrayAccessDefinition($expr->var) !== null) {
$this->fatalError($expr, 'Typed array increment/decrement requires an explicit checked element assignment');
}
$this->assertImmutableMutationTarget($expr->var);
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, str_repeat($op, 2));
@ -4172,6 +4184,9 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreDec(Expr\PreDec $expr): string
{
if ($this->getTypedArrayAccessDefinition($expr->var) !== null) {
$this->fatalError($expr, 'Typed array increment/decrement requires an explicit checked element assignment');
}
$this->assertImmutableMutationTarget($expr->var);
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '--');
@ -4747,10 +4762,15 @@ class CompilerBase implements PropertyAccessContext
protected function parseChainedExpr(NodeAbstract $node, string $op, bool $getValue = false): string
{
if ($op === self::OP_REFVAL) {
$this->assertTypedArrayReferenceForbidden($node);
$this->assertNativeArrayAccessReferenceForbidden($node);
$this->assertNativeObjectReferenceForbidden($node, $node);
$this->assertVariableReferenceStorage($node, $node);
}
if ($node instanceof Expr\ArrayDimFetch && $this->getTypedArrayAccessDefinition($node) !== null
&& in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) {
return $this->parseTypedArrayPresence($node, $op, $getValue);
}
if ($node instanceof Expr\ArrayDimFetch
&& $this->isNativeObjectClass($this->detectClassOfExpr($node->var))
) {

@ -40,6 +40,9 @@ trait CompilationStateTrait
foreach ($analysis['captures'] as $sourceName => $_) {
$name = $this->escapeVarName($sourceName);
if (isset($this->context->arguments[$name])) {
if (isset($this->context->typedArrays[$name])) {
$this->fatalError(new Variable($sourceName), 'Typed arrays cannot be captured by reference');
}
if (!empty($this->context->stdContainers[$name]['parameter'])) {
$this->fatalError(new Variable($sourceName), 'Std container parameters cannot be captured by reference');
}

@ -75,6 +75,8 @@ class FunctionContext
* @var array<string, array>
*/
public array $stdContainers = [];
/** @var array<string, array{kind: string, keyType: string, type: string, class: ?string}> */
public array $typedArrays = [];
public array $localVars = [];
/** @var array<string, string> Local variable => forced fallback storage type. */
public array $varTypeDegradations = [];

@ -21,6 +21,8 @@ class ArgInfo
public ?ArrayInitPlan $arrayInitPlan = null;
/** Explicit std-container contract; the call ABI remains php::Var. */
public ?array $stdContainer = null;
/** Strong PHP-array contract, with php::Array/php::Array & ABI. */
public ?array $typedArray = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?Expr $defaultExpr = null;
public ?Expr $defaultValue = null;

@ -10,7 +10,6 @@ namespace TypePhp\Entity;
use PhpParser\NodeAbstract;
use PhpParser\Modifiers;
use TypePhp\ArrayDef\ArrayDefinition;
class PropertyDef
{
@ -21,7 +20,8 @@ class PropertyDef
public ?ArrayInitPlan $arrayInitPlan = null;
/** Original declaration AST; lowered to $default only in the convert phase. */
public ?NodeAbstract $defaultExpr = null;
public ?ArrayDefinition $arrayDef = null;
public ?array $typedArray = null;
public ?array $stdContainer = null;
public bool $nullable = false;
/** The declared type is TypePHP's unconstrained, reference-capable `any` type. */
public bool $explicitAny = false;

@ -0,0 +1,13 @@
<?php
namespace TypePhp\Entity;
final readonly class TypedArrayPropertyWritePlan
{
public function __construct(
public bool $append,
public ?string $key,
public string $value,
) {
}
}

@ -170,6 +170,8 @@ trait CallArgumentGenerator
// expressions/temporaries may then be rearranged safely for the native
// C++ ABI without changing observable call order.
foreach ($sourceArgs as $sourceIndex => [$sourceArgIndex, $variadicName, $arg]) {
$typedParameter = $this->getArgInfoByIndex($functionDef, $sourceArgIndex);
$this->assertTypedArrayArgument($arg, $typedParameter, $typedParameter?->byRef ?? false, !$functionDef->stub);
if ($sourceIndex < $lastHoistingSourceIndex
&& $arg instanceof Node\Arg
&& !$arg->unpack
@ -472,6 +474,7 @@ trait CallArgumentGenerator
if ($this->isPlaceholderExpr($arg)) {
throw new PlaceHolder();
}
$this->validateTypedArrayDynamicArgument($arg, $funcName, $className, $i);
if ($arg->unpack) {
if ($hasNamedArg) {
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
@ -881,6 +884,7 @@ trait CallArgumentGenerator
if (count($expr->args) !== 1) {
$this->fatalError($errorNode, 'The std::ref function only accepts one parameter');
}
$this->assertTypedArrayReferenceForbidden($expr->args[0]->value);
return $expr->args[0]->value;
}
@ -888,6 +892,7 @@ trait CallArgumentGenerator
if (!empty($expr->args)) {
$this->fatalError($errorNode, 'The toRef method does not accept parameters');
}
$this->assertTypedArrayReferenceForbidden($expr->var);
return $expr->var;
}

@ -182,6 +182,9 @@ trait ClosureGenerator
foreach ($capturePlan['bindings'] as $binding) {
$this->addArgument($binding['name'], $binding['type']);
if ($binding['typedArray'] !== null) {
$this->context->typedArrays[$binding['name']] = $binding['typedArray'];
}
if ($binding['class'] !== '') {
$this->addObject($binding['name'], $binding['class']);
}
@ -217,7 +220,7 @@ trait ClosureGenerator
* @param list<Node\ClosureUse> $uses
* @return array{
* cpp: list<string>,
* bindings: list<array{name: string, type: string, class: string, immutable: bool, immutableObject: bool}>
* bindings: list<array{name: string, type: string, class: string, immutable: bool, immutableObject: bool, typedArray: ?array}>
* }|null
*/
private function buildNativeLocalClosureCapturePlan(array $uses): ?array
@ -271,6 +274,7 @@ trait ClosureGenerator
'class' => $valueType === Type::OBJECT ? $this->getDeclaredObjectType($name) : '',
'immutable' => isset($this->context->immutableVars[$name]),
'immutableObject' => isset($this->context->immutableObjectVars[$name]),
'typedArray' => $this->context->typedArrays[$name] ?? null,
];
}
return ['cpp' => $cpp, 'bindings' => $bindings];
@ -532,8 +536,17 @@ trait ClosureGenerator
foreach ($uses as $i => $useItem) {
$var = $this->parseIdentifier($useItem->var);
$code .= 'auto ' . $var . ' = vars_.get(' . $i . ');' . PHP_EOL;
$this->addArgument($var, Type::VAR);
if (isset($oriContext->typedArrays[$var])) {
if ($useItem->byRef) {
$this->fatalError($useItem, 'Typed arrays cannot be captured by reference');
}
$code .= 'php::Array ' . $var . ' = php::toArray(vars_.get(' . $i . '));' . PHP_EOL;
$this->addArgument($var, Type::ARRAY);
$this->context->typedArrays[$var] = $oriContext->typedArrays[$var];
} else {
$code .= 'auto ' . $var . ' = vars_.get(' . $i . ');' . PHP_EOL;
$this->addArgument($var, Type::VAR);
}
if (isset($oriContext->immutableVars[$var])) {
$this->context->immutableVars[$var] = true;
if (isset($oriContext->immutableObjectVars[$var])) {

@ -1427,7 +1427,7 @@ trait FuncCallOptimizer
// A function whose ABI contains Native pointers is callable
// only from generated TypePHP C++. It has no Zend wrapper and
// therefore must remain invisible to function_exists().
return $this->functionUsesNativeObject($this->getFunction($nativeFunction))
return $this->functionRequiresNativeAbi($this->getFunction($nativeFunction))
? 'false'
: 'true';
}

@ -192,6 +192,12 @@ trait ArrayExpressionTrait
protected function parseArrayDimFetch(Expr\ArrayDimFetch $node): string
{
if ($this->getTypedArrayAccessDefinition($node) !== null) {
if ($this->isArrayDimFetchUpdate($node)) {
$this->fatalError($node, 'Typed array writes must use checked element assignment');
}
return $this->parseTypedArrayRead($node);
}
if ($this->isNativeObjectClass($this->detectClassOfExpr($node->var))) {
if ($node->dim === null) {
$this->fatalError($node, 'Cannot use [] for reading');

@ -342,6 +342,10 @@ trait AssignOpTrait
{
$this->assertImmutableMutationTarget($left);
$this->recordImmutableAlias($left, $right);
$typedArrayAssignment = $this->parseTypedArrayAssignment($left, $right);
if ($typedArrayAssignment !== null) {
return $typedArrayAssignment;
}
$this->assertNotNullsafeWriteContext($left);
$this->assertNativeArrayAccessDirectWrite($left, true);
if ($left instanceof Expr\ArrayDimFetch
@ -732,9 +736,9 @@ trait AssignOpTrait
return $this->parseAssignPropertyArrayDim($left, $right);
} elseif ($this->isArrayDimFetch($left) and $this->isStaticPropertyFetch($left->var)) {
// Keep ordinary static-array writes on their established lowering
// path. Only ArrayDef needs the specialized key/value plan below.
// path. Only typed-array properties need the specialized key/value plan below.
$this->preparePropertyWriteTarget($left->var);
if ($this->getNativePropertyDef($left->var)?->arrayDef !== null) {
if ($this->getNativePropertyDef($left->var)?->typedArray !== null) {
return $this->parseAssignStaticPropertyArrayDim($left, $right);
}
}
@ -909,6 +913,11 @@ trait AssignOpTrait
protected function parseAssignOp(Expr\AssignOp $node, string $op): string
{
$this->assertImmutableMutationTarget($node->var);
if ($this->getTypedArrayDefinition($node->var) !== null
|| $this->getTypedArrayAccessDefinition($node->var) !== null) {
$this->fatalError($node, 'Typed array compound writes require an explicit checked element assignment');
}
if ($node->var instanceof Expr\ArrayDimFetch
&& !$this->canUpdateKnownArraySlotInPlace($node, $op)
) {
@ -1526,6 +1535,29 @@ trait AssignOpTrait
{
$this->assertImmutableMutationTarget($expr->var);
$this->assertImmutableMutationTarget($expr->expr);
if ($expr->var instanceof Expr\ArrayDimFetch || $expr->expr instanceof Expr\ArrayDimFetch) {
$this->assertTypedArrayReferenceForbidden($expr->var);
$this->assertTypedArrayReferenceForbidden($expr->expr);
}
if ($this->isVarExpr($expr->var) && $this->isVarExpr($expr->expr)) {
$definition = $this->getTypedArrayDefinition($expr->expr);
if ($definition !== null) {
$left = $this->parseWritableIdentifier($expr->var);
$right = $this->parseIdentifier($expr->expr);
if ($this->hasVar($left) || $this->context->scopeLevel !== 1
|| isset($this->context->varTypeDegradations[$left])) {
$this->fatalError($expr, 'Typed array aliases require a new top-level local');
}
$this->addTypedRefLocal($left, $right, Type::ARRAY_REF);
$this->context->typedArrays[$left] = $definition;
return $expr->getAttribute(self::ATTR_STATEMENT_EXPRESSION, false) ? '' : $left;
}
if ($this->getTypedArrayDefinition($expr->var) !== null) {
$this->fatalError($expr, 'Cannot rebind a typed array to untyped storage');
}
}
$this->assertTypedArrayReferenceForbidden($expr->var);
$this->assertTypedArrayReferenceForbidden($expr->expr);
$this->assertNativeArrayAccessReferenceForbidden($expr->var);
$this->assertNativeArrayAccessReferenceForbidden($expr->expr);
$this->assertNotNullsafeWriteContext($expr->var);
@ -1720,18 +1752,18 @@ trait AssignOpTrait
$propertyWriteTarget = $this->preparePropertyWriteTarget($left->var);
$code = '';
$value = $this->parseExprAsValue($right);
$arrayDefWrite = $this->prepareArrayDefDirectWrite($left, $right, $value);
if ($arrayDefWrite !== null) {
$value = $arrayDefWrite->value;
$typedArrayWrite = $this->prepareTypedArrayPropertyDirectWrite($left, $right, $value);
if ($typedArrayWrite !== null) {
$value = $typedArrayWrite->value;
}
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, Type::VAR);
if ($left->dim === null || ($arrayDefWrite !== null && $arrayDefWrite->append)) {
if ($left->dim === null || ($typedArrayWrite !== null && $typedArrayWrite->append)) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchAppendArray($left->var, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')';
}
$dim = $arrayDefWrite?->key ?? $this->parseIdentifier($left->dim);
$dim = $typedArrayWrite?->key ?? $this->parseIdentifier($left->dim);
return $code . '((' . $tmp . ' = ' . $value . ', ' . $this->emitDynamicPropertyFetchUpdateArray($left->var, $dim, $tmp, $propertyWriteTarget) . '), ' . $tmp . ')';
}
@ -1739,22 +1771,26 @@ trait AssignOpTrait
protected function parseAssignStaticPropertyArrayDim(Expr\ArrayDimFetch $left, Expr $right): string
{
$value = $this->parseExprAsValue($right);
$arrayDefWrite = $this->prepareArrayDefDirectWrite($left, $right, $value);
$typedArrayWrite = $this->prepareTypedArrayPropertyDirectWrite($left, $right, $value);
$array = $this->parseWritableIdentifier($left->var);
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, Type::VAR);
$value = $arrayDefWrite?->value ?? $value;
$value = $typedArrayWrite?->value ?? $value;
if ($left->dim === null || ($arrayDefWrite !== null && $arrayDefWrite->append)) {
if ($left->dim === null || ($typedArrayWrite !== null && $typedArrayWrite->append)) {
return '((' . $tmp . ' = ' . $value . ', ' . $array . '.newItem() = ' . $tmp . '), ' . $tmp . ')';
}
$dim = $arrayDefWrite?->key ?? $this->parseIdentifier($left->dim);
$dim = $typedArrayWrite?->key ?? $this->parseIdentifier($left->dim);
return '((' . $tmp . ' = ' . $value . ', ' . $array . '.item(' . $dim . ', true) = ' . $tmp . '), ' . $tmp . ')';
}
protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string
{
if ($this->getTypedArrayDefinition($expr->var) !== null
|| $this->getTypedArrayAccessDefinition($expr->var) !== null) {
$this->fatalError($expr, 'Typed array compound writes require an explicit checked element assignment');
}
$this->assertImmutableMutationTarget($expr->var);
$this->assertNativeArrayAccessDirectWrite($expr->var, false);
$this->checkLeftValue($expr->var);

@ -214,8 +214,12 @@ trait ForeachTrait
protected function parseForeach(Foreach_ $node): string
{
if (($typedArray = $this->getTypedArrayDefinition($node->expr)) !== null) {
return $this->parseTypedArrayForeach($node, $typedArray);
}
if ($node->byRef) {
$this->assertImmutableMutationTarget($node->expr);
$this->assertTypedArrayReferenceForbidden($node->expr);
}
$nativeClass = $this->detectClassOfExpr($node->expr);
if ($this->isNativeObjectClass($nativeClass)) {

@ -205,7 +205,7 @@ trait FunctionCallTrait
$resolvedTarget = $functionDef->getNamespacedName();
$expr->setAttribute('nativeCall', $nativeFn);
if ($expr->isFirstClassCallable()
&& $this->functionUsesNativeObject($functionDef)
&& $this->functionRequiresNativeAbi($functionDef)
) {
$this->fatalError($expr, 'Native ABI functions cannot be converted to Zend closures');
}
@ -298,6 +298,12 @@ trait FunctionCallTrait
}
$canOptimizeBuiltinFallback = $functionTarget['definitelyGlobal']
|| ($functionTarget['namespacedFallback'] && $globalName !== 'get_called_class');
if ($canOptimizeBuiltinFallback && in_array($globalName, ['array_key_exists', 'key_exists'], true)) {
$typedKeyExists = $this->parseTypedArrayKeyExistsCall($expr);
if ($typedKeyExists !== null) {
return $typedKeyExists;
}
}
$code = $canOptimizeBuiltinFallback
? $this->parseFuncCallWithOptimizer($globalName, $expr)
: false;

@ -1019,6 +1019,8 @@ trait PropertyAccessTrait
'offsetUnset',
[new Node\Arg($var->dim)],
) . ';';
} elseif ($this->getTypedArrayAccessDefinition($var) !== null) {
$lines[] = $this->parseTypedArrayUnset($var) . ';';
} elseif ($this->isStdContainerExpr($var)) {
$lines[] = $this->parseStdContainerOffsetUnset($var) . ';';
} else {

@ -24,17 +24,31 @@ use TypePhp\Transform\CompileTimeAttribute;
trait StdContainerTrait
{
protected function parseStdParameterDefinition(Node\Param $param): ?array
protected function validateStdAttributeType(Node\Param|Node\Stmt\Property $owner, string $name, string $storage): void
{
$type = $owner->type;
if ($type === null) {
return;
}
if ($type instanceof Node\Name || $type instanceof Node\Identifier) {
[$resolvedType] = $this->resolveTypeDecl($type, $owner instanceof Node\Param
? self::DECL_TYPE_OF_PARAM : self::DECL_TYPE_OF_PROPERTY);
if ($resolvedType === ($storage === 'box' ? Type::BOX : Type::ARRAY)) {
return;
}
}
$this->fatalError($owner, $name . ': a PHP type cannot also be declared unless it is ' . $storage);
}
protected function parseStdParameterDefinition(Node\Param|Node\Stmt\Property $param): ?array
{
foreach (['StdVector' => 'vector', 'StdMap' => 'map', 'StdOrderedMap' => 'orderedMap'] as $name => $method) {
$attribute = CompileTimeAttribute::find($param, $name);
if ($attribute === null) {
continue;
}
if ($param->type !== null) {
$this->fatalError($param, $name . ' is the parameter type declaration; a PHP type cannot also be declared');
}
if ($param->byRef || $param->variadic || $param->default !== null || $param->isPromoted()) {
$this->validateStdAttributeType($param, $name, 'box');
if ($param instanceof Node\Param && ($param->byRef || $param->variadic || $param->default !== null || $param->isPromoted())) {
$this->fatalError($param, $name . ' does not support reference, variadic, defaulted or promoted parameters');
}
$expected = $method === 'vector' ? 1 : 2;
@ -783,7 +797,7 @@ trait StdContainerTrait
return [
'type' => match ($expr->name->name) {
'Int', 'Float', 'Bool', 'BigInt', 'BigFloat', 'Decimal' => $this->parseStdNativeType($expr, $owner),
'String' => Type::STR,
'String', 'Str' => Type::STR,
'Array' => Type::ARRAY,
'Object' => Type::OBJECT,
'Any' => Type::VAR,
@ -933,7 +947,7 @@ trait StdContainerTrait
if (strcasecmp($className, 'Type') === 0 && $constName === 'Int') {
return Type::INT;
}
if (strcasecmp($className, 'Type') === 0 && $constName === 'String') {
if (strcasecmp($className, 'Type') === 0 && in_array($constName, ['String', 'Str'], true)) {
return Type::STR;
}
$this->fatalError($expr, "{$owner} key only supports Type::Int or Type::String");

@ -275,6 +275,7 @@ trait TypeConversionTrait
protected function convertToRef(NodeAbstract $expr): string
{
$this->assertTypedArrayReferenceForbidden($expr);
$this->assertNativeObjectReferenceForbidden($expr, $expr);
$this->checkLeftValue($expr);
if ($expr instanceof Node\Expr\ArrayDimFetch) {

@ -0,0 +1,419 @@
<?php
namespace TypePhp\Parser;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\TypedArrayPropertyWritePlan;
use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Type;
/** Compiler-enforced PHP-array contracts. Storage and COW remain php::Array's. */
trait TypedArrayTrait
{
protected function parseTypedArrayPropertyDefinition(Node\Stmt\Property $property): ?array
{
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) {
$attribute = CompileTimeAttribute::find($property, $name);
if ($attribute !== null) {
$this->validateStdAttributeType($property, $name, 'array');
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute);
}
}
return null;
}
protected function prepareTypedArrayPropertyDirectWrite(
Expr\ArrayDimFetch $left,
Expr $right,
string $value,
): ?TypedArrayPropertyWritePlan {
$definition = $this->getNativePropertyDef($left->var)?->typedArray;
if ($definition === null) {
return null;
}
$value = $this->guardTypedArrayValue($definition, $right, $value);
if ($left->dim === null) {
if ($definition['kind'] !== 'list') {
$this->fatalError($left, 'StdDict properties do not support append writes');
}
return new TypedArrayPropertyWritePlan(true, null, $value);
}
$key = $this->guardTypedArrayValue($definition, $left->dim, $this->parseExprAsValue($left->dim), true);
return new TypedArrayPropertyWritePlan(false, $key, $value);
}
protected function parseTypedArrayDefinition(string $kind, array $args, NodeAbstract $owner): array
{
$count = $kind === 'list' ? 1 : 2;
if (count($args) !== $count) {
$this->fatalError($owner, "Std{$kind} expects {$count} type argument(s)");
}
foreach ($args as $arg) {
if ($arg->name !== null || $arg->unpack || $arg->byRef) {
$this->fatalError($arg, 'Typed arrays require positional type arguments');
}
}
$key = $kind === 'list' ? Type::INT : $this->parseStdMapKeyType($args[0]->value, 'std::dict');
$value = $this->parseStdValueTypeInfo($args[$count - 1]->value, 'std::' . $kind);
if (!in_array($value['type'], [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY, Type::OBJECT, Type::VAR], true)) {
$this->fatalError($owner, 'Typed PHP arrays only support PHP value types or ClassName::class');
}
if ($this->isNativeObjectClass($value['class'] ?? '')) {
$this->fatalError($owner, 'Typed PHP arrays cannot hold Native objects');
}
// Integer-key lists/dicts share PHP storage; only list grants append.
return ['kind' => $kind, 'keyType' => $key, 'type' => $value['type'], 'class' => $value['class']];
}
protected function parseTypedArrayParameterDefinition(Node\Param $param): ?array
{
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) {
$attribute = CompileTimeAttribute::find($param, $name);
if ($attribute === null) {
continue;
}
$this->validateStdAttributeType($param, $name, 'array');
if ($param->variadic || $param->default !== null || $param->isPromoted()) {
$this->fatalError($param, $name . ' does not support variadic, defaulted or promoted parameters');
}
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute);
}
return null;
}
protected function getTypedArrayDefinition(NodeAbstract $expr): ?array
{
if ($expr instanceof Expr\ErrorSuppress) {
return $this->getTypedArrayDefinition($expr->expr);
}
if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) {
return $this->getTypedArrayDefinition($expr->expr);
}
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
&& $expr->name instanceof Node\Identifier && $this->isStdClassExpr($expr->class)
&& in_array(strtolower($expr->name->name), ['list', 'dict'], true)) {
return $this->parseTypedArrayDefinition(strtolower($expr->name->name), $expr->args, $expr);
}
if ($this->isVarExpr($expr)) {
return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null;
}
return null;
}
protected function getTypedArrayAccessDefinition(NodeAbstract $expr): ?array
{
return $expr instanceof Expr\ArrayDimFetch ? $this->getTypedArrayDefinition($expr->var) : null;
}
protected function assertTypedArrayReferenceForbidden(NodeAbstract $expr): void
{
while ($expr instanceof Expr\ArrayDimFetch) {
$expr = $expr->var;
}
if ($this->getTypedArrayDefinition($expr) !== null) {
$this->fatalError($expr, 'Typed arrays cannot escape through std::ref(), toRef(), or element references');
}
}
protected function parseTypedArrayAssignment(Expr $left, Expr $right): ?string
{
if ($this->getTypedArrayAccessDefinition($left) !== null) {
return $this->parseTypedArrayWrite($left, $right);
}
// Nested writes would bypass the declared outer element type.
if ($left instanceof Expr\ArrayDimFetch && $left->var instanceof Expr\ArrayDimFetch
&& $this->getTypedArrayAccessDefinition($left->var) !== null) {
$this->fatalError($left, 'Nested typed-array writes must replace a checked element');
}
if (!$this->isVarExpr($left)) {
return null;
}
$name = $this->parseWritableIdentifier($left);
$factory = $right instanceof Expr\StaticCall && $right->class instanceof Node\Name
&& $right->name instanceof Node\Identifier && $this->isStdClassExpr($right->class)
&& in_array(strtolower($right->name->name), ['list', 'dict'], true);
$definition = $factory
? $this->parseTypedArrayDefinition(strtolower($right->name->name), $right->args, $right)
: $this->getTypedArrayDefinition($right);
$existing = $this->context->typedArrays[$name] ?? null;
if ($definition === null && $existing === null) {
return null;
}
if ($existing !== null && $definition !== $existing) {
$this->fatalError($right, 'Typed array assignment requires an identical list/dict contract');
}
if ($this->hasScopeGlobalVar($name) || $this->hasStaticVar($name)
|| isset($this->context->varTypeDegradations[$name])) {
$this->fatalError($left, 'Typed arrays require fixed function-local storage and cannot be captured by reference');
}
if ($factory && $this->hasVar($name)) {
$this->fatalError($left, 'Typed array factories require a new variable');
}
if ($existing === null) {
if ($this->hasVar($name) && $this->getRawVarType($name) !== Type::ARRAY) {
$this->fatalError($left, 'Cannot erase a typed array into dynamic or reference storage');
}
// Do not retrofit a contract onto an already-live ordinary array:
// existing aliases or previous paths may still write untyped values.
if ($this->hasVar($name)) {
$this->fatalError($left, 'Typed array propagation requires a new variable');
}
$this->addLocalVar($name, Type::ARRAY);
$this->context->typedArrays[$name] = $definition;
}
return $name . ' = ' . ($factory ? 'php::Array{}' : $this->parseExprAsValue($right));
}
protected function guardTypedArrayValue(array $def, Expr $expr, string $code, bool $key = false): string
{
$this->assertExprCanBeUsedAsValue($expr, 'typed array element');
$expected = $key ? $def['keyType'] : $def['type'];
$class = $key ? '' : ($def['class'] ?? '');
$actual = Type::getReferencedType($this->detectTypeOfExpr($expr));
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) {
$this->fatalError($expr, 'Native objects cannot be stored in typed PHP arrays');
}
if ($key && $actual === Type::VAR) {
// Dynamic keys need a strict check, not a coercing conversion.
// These are internal PHPX helpers, not user keyword methods.
return ($expected === Type::INT ? 'php::toIntExact(' : 'php::toStringExact(') . $code . ')';
}
if (!$key && $expected === Type::VAR) {
return $code;
}
if ($class !== '') {
$actualClass = $this->detectClassOfExpr($expr);
if ($actualClass === '' || !$this->isObjectClassStaticallyAssignableTo($actualClass, $class)) {
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class);
}
if ($actual !== Type::OBJECT) {
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class);
}
return $code;
}
if ($actual !== $expected) {
$this->fatalError($expr, 'Typed array ' . ($key ? 'key' : 'value') . ' must have type ' . $expected);
}
if ($expected === Type::INT && $this->varIntTypes && $this->exprCanOverflowInt($expr)) {
$this->fatalError($expr, 'Typed array integer expressions that may widen require an explicit toInt() or native integer conversion');
}
return $code;
}
protected function convertTypedArrayRead(array $def, string $code): string
{
// Recover the statically proven type; no Exact checks or element scans.
return $this->convertExprFromType($def['type'], $code);
}
protected function parseTypedArrayKey(Expr\ArrayDimFetch $expr, bool $write = false): string
{
$def = $this->getTypedArrayAccessDefinition($expr);
if ($expr->dim === null) {
if (!$write || $def['kind'] !== 'list') {
$this->fatalError($expr, 'Only typed lists support append writes');
}
return '';
}
$key = $this->guardTypedArrayValue($def, $expr->dim, $this->parseExprAsValue($expr->dim), true);
return $key;
}
protected function parseTypedArrayRead(Expr\ArrayDimFetch $expr): string
{
$def = $this->getTypedArrayAccessDefinition($expr);
return $this->convertTypedArrayRead($def, $this->parseTypedArrayRawRead($expr));
}
protected function parseTypedArrayRawRead(Expr\ArrayDimFetch $expr): string
{
$array = $this->parseIdentifier($expr->var);
$key = $this->parseTypedArrayKey($expr);
return $array . '.offsetGet(' . $key . ')';
}
protected function parseTypedArrayPresence(Expr\ArrayDimFetch $expr, string $op, bool $getValue): string
{
$raw = $this->parseTypedArrayRawRead($expr);
if ($getValue) {
$result = $this->addTmpVar(Type::VAR);
$expr->setAttribute('chainOpResult', $result);
$raw = '(' . $result . ' = ' . $raw . ')';
}
return match ($op) {
self::OP_ISSET => 'php::exists(' . $raw . ')',
self::OP_EMPTY => 'php::empty(' . $raw . ')',
self::OP_NOT_EMPTY => 'php::notEmpty(' . $raw . ')',
default => $this->getChainedFunc($op) . '(' . $raw . ')',
};
}
protected function parseTypedArrayKeyExistsCall(Expr\FuncCall $call): ?string
{
if (count($call->args) !== 2) {
return null;
}
$arguments = [];
foreach ($call->args as $index => $argument) {
if ($argument->unpack || $argument->byRef) {
return null;
}
$name = $argument->name?->name ?? ($index === 0 ? 'key' : 'array');
if (!in_array($name, ['key', 'array'], true) || isset($arguments[$name])) {
return null;
}
$arguments[$name] = $argument->value;
}
if (!isset($arguments['key'], $arguments['array'])
|| $this->getTypedArrayDefinition($arguments['array']) === null) {
return null;
}
$definition = $this->getTypedArrayDefinition($arguments['array']);
// Snapshot source arguments in order, including named arguments.
$values = [];
foreach ($arguments as $name => $argument) {
if ($name === 'key') {
$key = $this->guardTypedArrayValue($definition, $argument, $this->parseExprAsValue($argument), true);
$values[$name] = $this->addTmpVar($definition['keyType']);
$this->context->beforeStmtLines[] = $values[$name] . ' = ' . $key . ';';
} else {
$values[$name] = $this->parseOrderedOperand($argument, false, true);
}
}
// Existing PHPX lookup retains PHP numeric-string normalization.
return $values['array'] . '.exists(' . $values['key'] . ')';
}
protected function parseTypedArrayForeach(Foreach_ $node, array $def): string
{
if ($node->byRef) {
$this->fatalError($node, 'Typed array elements cannot escape through foreach references');
}
$array = $this->parseIdentifier($node->expr);
$iterator = $this->genTmpVarName();
$assignments = '';
foreach ([[$node->keyVar, $def['keyType'], '', 'key'],
[$node->valueVar, $def['type'], $def['class'] ?? '', 'value']] as [$target, $type, $class, $part]) {
if ($target === null) {
continue;
}
if (!$this->isVarExpr($target)) {
$this->fatalError($target, 'Typed array foreach requires simple typed key/value variables');
}
$this->assertImmutableMutationTarget($target);
$name = $this->parseWritableIdentifier($target);
if (isset($this->context->varTypeDegradations[$name])) {
$this->fatalError($target, 'Typed foreach variables cannot be captured by reference');
}
if ($this->hasVar($name) && ($this->getRawVarType($name) !== $type
|| ($class !== '' && $this->getDeclaredObjectType($name) !== $class))) {
$this->fatalError($target, 'Typed array foreach variable must have the declared key/value type');
}
if (!$this->hasVar($name)) {
$this->addLocalVar($name, $type);
if ($class !== '') {
$this->addObject($name, $class);
}
}
// PHP normalizes numeric string keys to integers. Recover the
// declared key type here, without changing PHPX or array storage.
$assignments .= $this->getIndent() . ' ' . $name . ' = '
. $this->convertExprFromType($type, $iterator . '.' . $part . '()') . ';' . PHP_EOL;
}
$scope = $this->class ? $this->getLocalClassEntryPtr($this->getFullClassName()) : 'nullptr';
$code = '{' . PHP_EOL . $this->getIndent() . 'php::ForeachIterator ' . $iterator
. '{' . $array . ', false, ' . $scope . '};' . PHP_EOL
. $this->getIndent() . 'while (' . $iterator . '.next()) {' . PHP_EOL . $assignments;
$this->indentLevel++;
$body = $this->parseForeachBody($node);
$this->indentLevel--;
return $code . $this->parseBeforeStmtLines() . $body . $this->getIndent() . '}' . PHP_EOL
. $this->getIndent() . '}' . PHP_EOL;
}
protected function parseTypedArrayWrite(Expr\ArrayDimFetch $left, Expr $right): string
{
$def = $this->getTypedArrayAccessDefinition($left);
$array = $this->parseWritableIdentifier($left->var);
$key = $this->parseTypedArrayKey($left, true);
// Snapshot a key before lowering a RHS that might hoist side effects.
if ($key !== '') {
$keyVar = $this->addTmpVar($def['keyType']);
$this->context->beforeStmtLines[] = $keyVar . ' = ' . $key . ';';
$key = $keyVar;
}
$value = $this->guardTypedArrayValue($def, $right, $this->parseExprAsValue($right));
$result = $this->addTmpVar(Type::VAR);
// An assignment expression yields the assigned value, not void.
return '[&]() -> php::Var { ' . $result . ' = ' . $value . '; '
. $this->genTypedArrayStore($array, $key, $result) . '; return ' . $result . '; }()';
}
private function genTypedArrayStore(string $array, string $key, string $value): string
{
if ($key === '') {
return $array . '.appendValue(' . $value . ')';
}
return $array . '.offsetSet(' . $key . ', ' . $value . ')';
}
protected function parseTypedArrayUnset(Expr\ArrayDimFetch $expr): string
{
$array = $this->parseWritableIdentifier($expr->var);
$key = $this->parseTypedArrayKey($expr);
return $array . '.offsetUnset(' . $key . ')';
}
protected function assertTypedArrayArgument(Node\Arg $arg, ?ArgInfo $parameter, bool $byRef, bool $project): void
{
$expr = $arg->value;
if ($this->isReferenceWrapperCall($expr)) {
$expr = $this->unwrapReferenceWrapperCall($expr, $arg);
$this->assertTypedArrayReferenceForbidden($expr);
}
$def = $this->getTypedArrayDefinition($expr);
$expected = $parameter?->typedArray;
if ($expected !== null && $def !== $expected) {
$this->fatalError($arg, 'Typed array parameter requires an identical list/dict contract');
}
if ($def !== null && ($arg->unpack || ($project && $expected === null) || ($byRef && $expected === null))) {
$this->fatalError($arg, 'Typed arrays require matching annotated TypePHP parameters or read-only PHP calls');
}
if ($byRef && $expected === null) {
$this->assertTypedArrayReferenceForbidden($expr);
}
}
protected function validateTypedArrayDynamicArgument(Node\Arg $arg, string $function, string $class, int $index): void
{
$parameter = $arg->name !== null
? $this->getAotCallArgInfoByName($function, $class, $arg->name->name)
: $this->getAotCallArgInfo($function, $class, $index);
if ($parameter?->typedArray !== null) {
$this->fatalError($arg, 'Typed array parameters require a statically resolved native TypePHP call, not Zend dispatch');
}
$byRef = $function !== '' && ($arg->name !== null
? $this->isReferenceNamedArgument($function, $class, $arg->name->name)
: $this->isReferenceArgument($function, $class, $index));
$this->assertTypedArrayArgument($arg, $parameter, $byRef, $parameter !== null);
}
protected function functionUsesTypedArray(\TypePhp\Entity\FunctionDef $function): bool
{
foreach ($function->argInfoList as $parameter) {
if ($parameter->typedArray !== null) {
return true;
}
}
return false;
}
protected function functionRequiresNativeAbi(\TypePhp\Entity\FunctionDef $function): bool
{
return $this->functionUsesNativeObject($function) || $this->functionUsesTypedArray($function);
}
}

@ -601,6 +601,10 @@ trait UniversalMethodCall
*/
protected function parseUniversalMethodCall(Node\Expr\MethodCall $expr, string $receiver, string $method, array $def, bool $isVar = true): ?string
{
if ($this->getTypedArrayDefinition($expr->var) !== null
&& in_array($def['handler'], ['php_fn_ref', 'direct_method_mutate', 'provider_extension'], true)) {
$this->fatalError($expr, 'Typed arrays cannot use untyped mutating or extension methods');
}
if ($def['handler'] === 'php_fn') {
$function = strtolower($def['fn']);
$this->compilationStatistics->record(CompilationStatistics::FUNCTIONS, $function);
@ -638,6 +642,23 @@ trait UniversalMethodCall
}
$this->validateUniversalMethodArgs($expr, $method, $def, $isVar);
if ($this->getTypedArrayDefinition($expr->var) !== null && in_array($method, ['keyExists', 'get'], true)) {
$argument = $expr->args[0];
if ($argument->unpack || $argument->name !== null || $argument->byRef) {
$this->fatalError($argument, 'Typed array lookup methods require a positional key');
}
$key = $this->guardTypedArrayValue($this->getTypedArrayDefinition($expr->var), $argument->value,
$this->parseExprAsValue($argument->value), true);
return $receiver . ($method === 'keyExists' ? '.exists(' : '.get(') . $key . ')';
}
if (in_array($def['handler'], ['php_fn', 'php_fn_ref'], true)) {
$position = $def['receiver_pos'] ?? 0;
foreach ($expr->args as $index => $argument) {
$parameterIndex = $position === 0 || $index >= $position - 1 ? $index + 1 : $index;
$this->validateTypedArrayDynamicArgument($argument, $def['fn'], '', $parameterIndex);
}
}
return match ($def['handler']) {
'calc_op' => $this->genUniversalCalcOp($receiver, $def['op'], $expr->args),
'calc_inc' => '(' . $receiver . ' + 1)',

@ -1357,7 +1357,6 @@ class Preprocessor extends CompilerBase
// not to the property default table. The property itself must stay
// uninitialized until __construct assigns it.
$promotedProperty = $this->addClassProperty($phpName, $param->flags, $param->type, null, $nullable, $param, true);
$promotedProperty->arrayDef = $this->parseArrayDefinition($param);
}
if ($param->variadic) {
if ($i !== $last) {
@ -1377,11 +1376,22 @@ class Preprocessor extends CompilerBase
}
$argInfo = new ArgInfo();
$argInfo->stdContainer = $this->parseStdParameterDefinition($param);
$argInfo->typedArray = $this->parseTypedArrayParameterDefinition($param);
if ($argInfo->typedArray !== null && $functionDef->generator) {
$this->fatalError($param, 'Typed array parameters are not supported on generators');
}
if ($argInfo->stdContainer !== null && $functionDef->generator) {
$this->fatalError($param, 'Std container parameter attributes are not supported on generators');
}
$type = $this->parseParameterType($param, $argInfo, $name);
if ($argInfo->typedArray !== null) {
$type = $param->byRef ? Type::ARRAY_REF : Type::ARRAY;
$argInfo->undeclared = false;
}
if ($argInfo->stdContainer !== null) {
// box is the public storage declaration. Containers still use
// the existing php::Var resource ABI, not a raw php::Box.
$type = Type::VAR;
$argInfo->undeclared = false;
}
$argInfo->name = $name;
@ -1392,7 +1402,7 @@ class Preprocessor extends CompilerBase
$argInfo->property = $param->isPromoted();
$argInfo->immutable = \TypePhp\Transform\CompileTimeAttribute::consume($param, 'Immutable');
if ($param->type === null || $param->type instanceof NullableType) {
$argInfo->nullable = $argInfo->stdContainer === null;
$argInfo->nullable = $argInfo->stdContainer === null && $argInfo->typedArray === null;
}
if (($param->byRef && $param->type !== null && !Type::isTypedRefType($type))
|| $param->type instanceof NullableType
@ -2747,9 +2757,10 @@ class Preprocessor extends CompilerBase
protected function parseClassPropertyDef(Node\Stmt\Property $v): void
{
$this->validateClassPropertyHookPlacement($v);
$arrayDef = $this->parseArrayDefinition($v);
$typedArray = $this->parseTypedArrayPropertyDefinition($v);
$stdContainer = $this->parseStdParameterDefinition($v);
if ($this->classDef->nativeObject) {
if ($v->type === null) {
if ($v->type === null && $typedArray === null && $stdContainer === null) {
$this->fatalError($v, 'Native class properties must declare a type');
}
if ($v->isStatic()) {
@ -2771,8 +2782,13 @@ class Preprocessor extends CompilerBase
foreach ($v->props as $prop) {
$propName = $this->parseIdentifier($prop->name);
$propDef = $this->addClassProperty($propName, $v->flags, $v->type, $prop->default, $nullable, $v);
$propDef->arrayDef = $arrayDef;
$propertyType = $v->type ?? ($typedArray !== null ? new Node\Identifier('array') : null);
if ($propertyType === null && $stdContainer !== null) {
$propertyType = new Node\Name('box');
}
$propDef = $this->addClassProperty($propName, $v->flags, $propertyType, $prop->default, $nullable, $v);
$propDef->typedArray = $typedArray;
$propDef->stdContainer = $stdContainer;
if ($this->classDef->nativeObject && $this->isNativeObjectForbiddenPropertyType($propDef)) {
$message = $propDef->type === Type::BOX
? 'Native class properties cannot use Box types'

@ -20,7 +20,7 @@ final class CompileTimeAttribute
return;
}
foreach ($node->getParams() as $parameter) {
foreach (['StdVector', 'StdMap', 'StdOrderedMap'] as $name) {
foreach (['StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict'] as $name) {
$attribute = self::find($parameter, $name);
if ($attribute !== null) {
throw new CompileTimeAttributeError(

@ -25,7 +25,6 @@ final class CompileTimeAttributeRegistry
public const ARGUMENTS_FIELDS = 'fields';
public const ARGUMENTS_VALIDATE = 'validate';
public const ARGUMENTS_WASM_EXPORT = 'wasm_export';
public const ARGUMENTS_ARRAY_DEF = 'array_def';
public const ARGUMENTS_STD_CONTAINER = 'std_container';
public const PHASE_PREPROCESS = 'preprocess';
@ -101,10 +100,9 @@ final class CompileTimeAttributeRegistry
$add('Hot', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Hot can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Cold']);
$add('Cold', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Cold can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Hot']);
$add('Constructor', [self::TARGET_DECLARED_PROPERTY], 'Constructor can only be applied to instance properties', self::ARGUMENTS_NONE, self::PHASE_CLASS_LEAVE);
$add('ArrayDef', [self::TARGET_PROPERTY], 'ArrayDef can only be applied to properties', self::ARGUMENTS_ARRAY_DEF, self::PHASE_PREPROCESS);
$containerAttributes = ['StdVector', 'StdMap', 'StdOrderedMap'];
$containerAttributes = ['StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict'];
foreach ($containerAttributes as $name) {
$add($name, [self::TARGET_PARAMETER], $name . ' can only be applied to function or method parameters', self::ARGUMENTS_STD_CONTAINER, self::PHASE_PREPROCESS, true, array_values(array_diff($containerAttributes, [$name])));
$add($name, [self::TARGET_PARAMETER, self::TARGET_DECLARED_PROPERTY], $name . ' can only be applied to function or method parameters or properties', self::ARGUMENTS_STD_CONTAINER, self::PHASE_PREPROCESS, true, array_values(array_diff($containerAttributes, [$name])));
}
return $definitions;

@ -1463,7 +1463,7 @@ CODE;
if ($functionDef->method) {
continue;
}
if ($this->functionUsesNativeObject($functionDef)) {
if ($this->functionRequiresNativeAbi($functionDef)) {
continue;
}
$fullName = $functionDef->getNamespacedName();
@ -3010,15 +3010,13 @@ CODE;
public function isNativeFunctionForStub(string $function): bool
{
return $this->hasFunction($function)
&& $this->functionUsesNativeObject($this->getFunction($function));
&& $this->functionRequiresNativeAbi($this->getFunction($function));
}
public function isNativeMethodForStub(string $class, string $method): bool
{
$class = ltrim($class, '\\');
return $this->hasClass($class)
&& $this->getClass($class)->hasMethod($method)
&& $this->functionUsesNativeObject($this->getClass($class)->getMethod($method)->functionDef);
$function = $this->findAotMethodFunctionDef(ltrim($class, '\\'), $method);
return $function !== null && $this->functionRequiresNativeAbi($function);
}
public function getArgInfoHeaderFile(string $file, bool $relative = false): string
@ -4209,7 +4207,7 @@ CODE;
}
foreach ($this->functionDefineInFile as $functionDef) {
if ($functionDef->attributeFactory || $this->functionUsesNativeObject($functionDef)) {
if ($functionDef->attributeFactory || $this->functionRequiresNativeAbi($functionDef)) {
continue;
}
$cppCode .= $this->genFunctionWrapper($functionDef);
@ -5339,7 +5337,8 @@ CODE;
$typeA = $paramA->type ? $this->typeNodeToString($paramA->type) : null;
$typeB = $paramB->type ? $this->typeNodeToString($paramB->type) : null;
if ($typeA !== $typeB
|| $this->parseStdParameterDefinition($paramA) !== $this->parseStdParameterDefinition($paramB)) {
|| $this->parseStdParameterDefinition($paramA) !== $this->parseStdParameterDefinition($paramB)
|| $this->parseTypedArrayParameterDefinition($paramA) !== $this->parseTypedArrayParameterDefinition($paramB)) {
$this->fatalError(
$classStmt,
"Trait `{$traitA}` and Trait `{$traitB}` define the same abstract method `{$methodName}` " .
@ -6030,7 +6029,7 @@ CODE;
}
$methods = $classDef->methods;
foreach ($methods as $methodDef) {
if ($this->functionUsesNativeObject($methodDef->functionDef)) {
if ($this->functionRequiresNativeAbi($methodDef->functionDef)) {
continue;
}
$cppCode .= $this->genMethodWrapper($classDef, $methodDef);
@ -6094,6 +6093,9 @@ CODE;
? Type::ARRAY
: ($this->getNativeObjectArgumentType($argInfo) ?? $argInfo->type);
$this->addArgument($argInfo->name, $argumentType);
if ($argInfo->typedArray !== null) {
$this->context->typedArrays[$argInfo->name] = $argInfo->typedArray;
}
if (!$argInfo->variadic and $argInfo->declaredClass) {
$this->addObject($argInfo->name, $argInfo->declaredClass);
}
@ -6928,6 +6930,9 @@ CODE;
private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool
{
if ($childArg->typedArray !== null || $parentArg->typedArray !== null) {
return $childArg->typedArray !== null && $childArg->typedArray === $parentArg->typedArray;
}
// Child methods may omit parameter types (contravariance — accepting a
// wider set of inputs is always compatible with the parent contract).
if ($this->isTopParameterType($childArg)) {
@ -8371,7 +8376,8 @@ CODE;
|| $existing->type !== $incoming->type
|| $existing->class !== $incoming->class
|| $existing->nullable !== $incoming->nullable
|| $existing->arrayDef != $incoming->arrayDef
|| $existing->typedArray != $incoming->typedArray
|| $existing->stdContainer != $incoming->stdContainer
) {
return false;
}
@ -8449,14 +8455,14 @@ CODE;
}
foreach ($classDef->methods as $methodDef) {
if (!$methodDef->functionDef->abstractMethod
&& !$this->functionUsesNativeObject($methodDef->functionDef)) {
&& !$this->functionRequiresNativeAbi($methodDef->functionDef)) {
$code .= $this->genMethodWrapper($classDef, $methodDef);
}
}
}
foreach ($this->symbols->functions() as $functionDef) {
if ($functionDef->stub && !$functionDef->method && !$functionDef->attributeFactory
&& !$this->functionUsesNativeObject($functionDef)) {
&& !$this->functionRequiresNativeAbi($functionDef)) {
$code .= $this->genFunctionWrapper($functionDef);
}
}

@ -115,32 +115,36 @@ final readonly class Constructor
{
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class ArrayDef
{
public function __construct(string $keyOrValueType, ?string $valueType = null)
{
}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
final readonly class StdVector
{
public function __construct(string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
final readonly class StdMap
{
public function __construct(string $keyType, string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
final readonly class StdOrderedMap
{
public function __construct(string $keyType, string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
final readonly class StdList
{
public function __construct(string $valueType) {}
}
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
final readonly class StdDict
{
public function __construct(string $keyType, string $valueType) {}
}
/**
* Public compile-time type symbols shared by MethodsFor providers and std containers.
* This root class is deliberately distinct from the compiler-internal TypePhp\Type.
@ -154,6 +158,7 @@ final class Type
public const string BigFloat = 'bigfloat';
public const string Decimal = 'decimal';
public const string String = 'string';
public const string Str = 'string';
public const string Array = 'array';
public const string Object = 'object';
public const string Any = 'any';
@ -255,4 +260,14 @@ class std
{
return [];
}
public static function list(mixed $valueType): array
{
return [];
}
public static function dict(mixed $keyType, mixed $valueType): array
{
return [];
}
}

@ -1,84 +0,0 @@
--TEST--
ArrayDef checks dynamic compound and high-precision value types
--FILE--
<?php
class ArrayDefCompoundBox
{
#[ArrayDef(Type::Array)]
public array $arrays = [];
#[ArrayDef(Type::Object)]
public array $objects = [];
#[ArrayDef(Type::BigInt)]
public array $bigInts = [];
#[ArrayDef(Type::BigFloat)]
public array $bigFloats = [];
#[ArrayDef(Type::Decimal)]
public array $decimals = [];
}
function putCompound(ArrayDefCompoundBox $box, int $slot, any $value): void
{
if ($slot === 0) {
$box->arrays[] = $value;
} elseif ($slot === 1) {
$box->objects[] = $value;
} elseif ($slot === 2) {
$box->bigInts[] = $value;
} elseif ($slot === 3) {
$box->bigFloats[] = $value;
} else {
$box->decimals[] = $value;
}
}
function main(): void
{
$box = new ArrayDefCompoundBox();
putCompound($box, 0, [1, 2]);
putCompound($box, 1, new stdClass());
putCompound($box, 2, std::bigInt(3));
putCompound($box, 3, std::bigFloat('4'));
putCompound($box, 4, std::decimal('5'));
var_dump($box->arrays[0]);
var_dump($box->objects[0] instanceof stdClass);
echo std::bigInt($box->bigInts[0])->toString(), "\n";
echo std::bigFloat($box->bigFloats[0])->toString(), "\n";
echo std::decimal($box->decimals[0])->toString(), "\n";
try {
putCompound($box, 0, 'not-array');
} catch (TypeError $error) {
echo "array checked\n";
}
try {
putCompound($box, 1, []);
} catch (TypeError $error) {
echo "object checked\n";
}
try {
putCompound($box, 2, std::decimal('6'));
} catch (TypeError $error) {
echo "BigInt checked\n";
}
}
?>
--EXPECT--
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
bool(true)
3
4
5
array checked
object checked
BigInt checked

@ -0,0 +1,71 @@
--TEST--
Std attributes accept matching array/box storage types and sparse property list writes
--FILE--
<?php
class PropertyState
{
#[StdList(Type::Int)] public array $values = [];
#[StdDict(Type::Str, Type::Int)] public array $counts = [];
#[StdList(Type::Str)] public $names = [];
#[StdDict(Type::Int, Type::Str)] public static array $labels = [];
#[StdVector(Type::Int)] public box $vector;
}
#[Native]
class NativePropertyState
{
#[StdList(Type::Int)] public $values = [];
}
function append_vector(#[StdVector(Type::Int)] box $v): void { $v[] = 5; }
function write_map(#[StdMap(Type::Str, Type::Int)] box $m): void { $m['a'] = 6; }
function write_ordered(#[StdOrderedMap(Type::Int, Type::Str)] box $m): void { $m[1] = 'one'; }
function copy_list(#[StdList(Type::Int)] array $a): void { $a[] = 7; }
function append_list(#[StdList(Type::Int)] array &$a): void { $a[] = 8; }
function write_dict(#[StdDict(Type::Str, Type::Int)] array &$a): void { $a['a'] = 9; }
function write_property(PropertyState $s, $key): void { $s->values[$key] = 40; }
function main(): void
{
$v = std::vector(Type::Int);
append_vector($v);
echo $v[0], "\n";
$m = std::map(Type::Str, Type::Int);
write_map($m);
echo $m['a'], "\n";
$o = std::orderedMap(Type::Int, Type::Str);
write_ordered($o);
echo $o[1], "\n";
$a = std::list(Type::Int);
copy_list($a);
append_list($a);
echo count($a), ':', $a[0], "\n";
$d = std::dict(Type::Str, Type::Int);
write_dict($d);
echo $d['a'], "\n";
$s = new PropertyState();
$s->values[-5] = 10;
$s->values[100] = 20;
$s->values[] = 30;
write_property($s, 200);
echo $s->values[-5], ':', $s->values[100], ':', $s->values[101], ':', $s->values[200], "\n";
try { write_property($s, '200'); }
catch (TypeError $error) { echo "strict property key\n"; }
$s->counts['123'] = 2;
$s->names[100] = 'sparse';
PropertyState::$labels[-3] = 'negative';
echo $s->counts['123'], ':', $s->names[100], ':', PropertyState::$labels[-3], "\n";
$n = new NativePropertyState();
$n->values[-2] = 11;
$n->values[100] = 12;
$n->values[] = 13;
echo $n->values[-2], ':', $n->values[100], ':', $n->values[101], "\n";
}
?>
--EXPECT--
5
6
one
1:8
9
10:20:30:40
strict property key
2:sparse:negative
11:12:13

@ -0,0 +1,57 @@
--TEST--
Typed PHP arrays strictly check dynamic keys without changing PHPX or coercing keys
--FILE--
<?php
function main(): void
{
$list = std::list(Type::Int);
$key = std::any(50);
$list[$key] = 7;
var_dump($list[$key], isset($list[$key]), empty($list[$key]));
var_dump(array_key_exists($key, $list), $list->keyExists($key), $list->get($key));
unset($list[$key]);
var_dump(isset($list[$key]), count($list));
$dict = std::dict(Type::Str, Type::Int);
$name = std::any('123');
$dict[$name] = 9;
var_dump($dict[$name], key_exists(array: $dict, key: $name), $dict->get($name));
foreach ($dict as $stringKey => $value) {
var_dump(is_string($stringKey), $stringKey, $value);
}
try { $list[std::any('50')] = 1; } catch (TypeError $error) { echo "write rejected\n"; }
try { var_dump($dict[std::any(123)]); } catch (TypeError $error) { echo "read rejected\n"; }
try { var_dump(isset($list[std::any(true)])); } catch (TypeError $error) { echo "isset rejected\n"; }
try { unset($dict[std::any(1.5)]); } catch (TypeError $error) { echo "unset rejected\n"; }
try { var_dump(array_key_exists(std::any('50'), $list)); } catch (TypeError $error) { echo "key exists rejected\n"; }
try { var_dump($dict->get(std::any(123))); } catch (TypeError $error) { echo "get rejected\n"; }
var_dump(count($list), count($dict));
unset($dict[$name]);
var_dump(count($dict));
}
?>
--EXPECT--
int(7)
bool(true)
bool(false)
bool(true)
bool(true)
int(7)
bool(false)
int(0)
int(9)
bool(true)
int(9)
bool(true)
string(3) "123"
int(9)
write rejected
read rejected
isset rejected
unset rejected
key exists rejected
get rejected
int(0)
int(1)
int(0)

@ -0,0 +1,46 @@
--TEST--
Typed array explicit key conversions, readonly calls and dynamic by-value COW isolation
--FILE--
<?php
function dynamic_mutator(array &$array): void { $array[] = 'wrong'; }
function main(): void
{
$list = std::list(Type::Int);
$key = std::any(50);
$list[$key->toInt()] = 7;
$list[] = 8;
var_dump($list[50], array_search(8, $list, true), count(array_keys($list)));
$callback = 'dynamic_mutator';
// Zend warns about the unknown reference signature; it receives a value
// snapshot, never a reference to the statically typed array.
@$callback($list);
$alias =& $list;
@$callback($alias);
var_dump(count($list));
$dict = std::dict(Type::Str, Type::Int);
$stringKey = std::any('123');
$dict[$stringKey->toString()] = 9;
var_dump(array_keys($dict), $dict['123']);
var_dump(array_key_exists('123', $dict), key_exists(array: $dict, key: '123'));
var_dump($dict->keyExists('123'), $dict->get('123'), $dict->keyExists('missing'));
$list[std::any('50')->toInt()] = 10;
var_dump($list[50], count($list));
}
?>
--EXPECT--
int(7)
int(51)
int(2)
int(2)
array(1) {
[0]=>
int(123)
}
int(9)
bool(true)
bool(true)
bool(true)
int(9)
bool(false)
int(10)
int(2)

@ -0,0 +1,55 @@
--TEST--
Typed list/dict class values propagate through foreach and native method parameters
--FILE--
<?php
use StdList as ListOf;
class TypedUser
{
public function __construct(public int $id) {}
public function name(): string { return 'user'; }
}
class TypedReceiver
{
public function fill(#[StdDict(Type::Str, TypedUser::class)] &$users): void
{
$users['123'] = new TypedUser(9);
$users['alice'] = new TypedUser(7);
}
}
function show(#[ListOf(TypedUser::class)] $users): void
{
foreach ($users as $key => $user) {
echo $key, ':', $user->id, ':', $user->name(), "\n";
}
}
function main(): void
{
$list = std::list(TypedUser::class);
$list[] = new TypedUser(1);
show($list);
$dict = std::dict(Type::Str, TypedUser::class);
$receiver = new TypedReceiver();
$receiver->fill($dict);
$copy = $dict;
foreach ($dict as $name => $user) {
var_dump(is_string($name), $name, $user->id);
}
var_dump($dict['123']->id, isset($dict['123']), empty($dict['123']));
unset($dict['123']);
var_dump(isset($dict['123']), isset($copy['123']), $copy['123']->id);
}
?>
--EXPECT--
0:1:user
bool(true)
string(3) "123"
int(9)
bool(true)
string(5) "alice"
int(7)
int(9)
bool(true)
bool(false)
bool(false)
bool(true)
int(9)

@ -1,5 +1,5 @@
--TEST--
ArrayDef supports class value types, subclasses, aliases and dynamic checks
TypedProperty supports class value types, subclasses, aliases and statically typed writes
--FILE--
<?php
@ -18,19 +18,19 @@ namespace Demo {
class UserCollection
{
#[\ArrayDef(Member::class)]
#[\StdList(Member::class)]
public array $list = [];
#[\ArrayDef(\Type::String, \App\User::class)]
#[\StdDict(\Type::String, \App\User::class)]
public array $map = [];
}
function putList(UserCollection $collection, any $value): void
function putList(UserCollection $collection, \App\User $value): void
{
$collection->list[] = $value;
}
function putMap(UserCollection $collection, any $key, any $value): void
function putMap(UserCollection $collection, any $key, \App\User $value): void
{
$collection->map[$key] = $value;
}
@ -53,16 +53,6 @@ namespace {
echo $key, '=', $user->name, "\n";
}
try {
Demo\putList($collection, new App\Other());
} catch (TypeError $error) {
echo "list class checked\n";
}
try {
Demo\putMap($collection, 'bad', new stdClass());
} catch (TypeError $error) {
echo "map class checked\n";
}
}
}
?>
@ -72,5 +62,3 @@ admin
dynamic-list
owner=owner
dynamic=dynamic-map
list class checked
map class checked

@ -1,34 +1,34 @@
--TEST--
ArrayDef list indexed writes use PHP's append position after unset
TypedProperty lists allow sparse and negative integer keys after unset
--FILE--
<?php
class ArrayDefListWithHoles
class TypedPropertyListWithHoles
{
#[ArrayDef(Type::String)]
#[StdList(Type::String)]
public array $values = [];
}
#[Native]
class NativeArrayDefListWithHoles
class NativeTypedPropertyListWithHoles
{
#[ArrayDef(Type::Int)]
#[StdList(Type::Int)]
public array $values = [];
}
function writeListValue(ArrayDefListWithHoles $box, int $index, string $value): void
function writeListValue(TypedPropertyListWithHoles $box, int $index, string $value): void
{
$box->values[$index] = $value;
}
function writeNativeListValue(NativeArrayDefListWithHoles $box, int $index, int $value): void
function writeNativeListValue(NativeTypedPropertyListWithHoles $box, int $index, int $value): void
{
$box->values[$index] = $value;
}
function main(): void
{
$box = new ArrayDefListWithHoles();
$box = new TypedPropertyListWithHoles();
writeListValue($box, 0, 'zero');
$box->values[] = 'one';
$box->values[] = 'two';
@ -42,13 +42,11 @@ function main(): void
var_dump($box->values);
try {
writeListValue($box, 6, 'gap');
} catch (Error $error) {
echo "list gap rejected\n";
}
writeListValue($box, 6, 'gap');
writeListValue($box, -2, 'negative');
var_dump($box->values[6], $box->values[-2]);
$native = new NativeArrayDefListWithHoles();
$native = new NativeTypedPropertyListWithHoles();
$native->values[] = 10;
$native->values[] = 20;
unset($native->values[1]);
@ -69,7 +67,8 @@ array(4) {
[1]=>
string(9) "one-again"
}
list gap rejected
string(3) "gap"
string(8) "negative"
array(2) {
[0]=>
int(10)

@ -0,0 +1,32 @@
--TEST--
StdList and StdDict properties support statically typed PHP array/object/any values
--FILE--
<?php
class PhpValueProperties
{
#[StdList(Type::Array)] public array $arrays = [];
#[StdList(Type::Object)] public array $objects = [];
#[StdDict(Type::Str, Type::Any)] public array $values = [];
}
function putArray(PhpValueProperties $s, array $value): void { $s->arrays[] = $value; }
function putObject(PhpValueProperties $s, object $value): void { $s->objects[] = $value; }
function main(): void
{
$s = new PhpValueProperties();
putArray($s, [1, 2]);
putObject($s, new stdClass());
$s->values['int'] = 3;
$s->values['str'] = 'four';
var_dump($s->arrays[0], $s->objects[0] instanceof stdClass, $s->values['int'], $s->values['str']);
}
?>
--EXPECT--
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
bool(true)
int(3)
string(4) "four"

@ -1,61 +1,61 @@
--TEST--
ArrayDef enforces direct list and map writes for Zend and Native classes
TypedProperty enforces direct list and map writes for Zend and Native classes
--FILE--
<?php
class ZendArrayDefBox
class ZendTypedPropertyBox
{
#[ArrayDef(Type::String)]
#[StdList(Type::String)]
public array $names = [];
#[ArrayDef(Type::Int, Type::String)]
#[StdDict(Type::Int, Type::String)]
public array $labels = [];
#[ArrayDef(Type::String, Type::Int)]
#[StdDict(Type::String, Type::Int)]
public static array $staticCounters = [];
}
class PromotedArrayDefBox
class PromotedTypedPropertyBox
{
public function __construct(
#[ArrayDef(Type::Int)] public array $values = [],
public array $values = [],
) {
}
}
#[Native]
class NativeArrayDefBox
class NativeTypedPropertyBox
{
#[ArrayDef(Type::Int)]
#[StdList(Type::Int)]
public array $values = [];
#[ArrayDef(Type::String, Type::Int)]
#[StdDict(Type::String, Type::Int)]
public array $counters = [];
}
function writeDynamicList(ZendArrayDefBox $box, any $key, any $value): void
function writeDynamicList(ZendTypedPropertyBox $box, any $key, string $value): void
{
$box->names[$key] = $value;
}
function writeDynamicMap(NativeArrayDefBox $box, any $key, any $value): void
function writeDynamicMap(NativeTypedPropertyBox $box, any $key, int $value): void
{
$box->counters[$key] = $value;
}
function main(): void
{
$zend = new ZendArrayDefBox();
$zend = new ZendTypedPropertyBox();
$zend->names[] = 'first';
$zend->names[count($zend->names)] = 'second';
$zend->names[0] = 'changed';
$zend->labels[10] = 'ten';
ZendArrayDefBox::$staticCounters['writes'] = 1;
ZendTypedPropertyBox::$staticCounters['writes'] = 1;
$promoted = new PromotedArrayDefBox();
$promoted = new PromotedTypedPropertyBox();
$promoted->values[] = 13;
$native = new NativeArrayDefBox();
$native = new NativeTypedPropertyBox();
$native->values[] = 7;
$native->values[count($native->values)] = 8;
$native->values[1] = 9;
@ -65,33 +65,20 @@ function main(): void
writeDynamicList($zend, count($zend->names), 'appended');
writeDynamicMap($native, 'dynamic', 12);
var_dump($zend->names, $zend->labels, ZendArrayDefBox::$staticCounters, $promoted->values, $native->values, $native->counters);
var_dump($zend->names, $zend->labels, ZendTypedPropertyBox::$staticCounters, $promoted->values, $native->values, $native->counters);
try {
writeDynamicList($zend, '1', 'bad-key');
} catch (TypeError $error) {
echo "list key type checked\n";
}
try {
writeDynamicList($zend, 0, 123);
} catch (TypeError $error) {
echo "list value type checked\n";
}
try {
writeDynamicMap($native, 1, 12);
} catch (TypeError $error) {
echo "map key type checked\n";
}
try {
writeDynamicMap($native, 'bad', '12');
} catch (TypeError $error) {
echo "map value type checked\n";
}
try {
writeDynamicList($zend, count($zend->names) + 1, 'out');
} catch (Error $error) {
echo "list bounds checked\n";
}
writeDynamicList($zend, count($zend->names) + 1, 'out');
var_dump($zend->names[4]);
}
?>
--EXPECT--
@ -128,7 +115,5 @@ array(2) {
int(12)
}
list key type checked
list value type checked
map key type checked
map value type checked
list bounds checked
string(3) "out"

@ -0,0 +1,62 @@
--TEST--
Typed PHP arrays retain COW storage, native references, sparse keys and foreach types
--FILE--
<?php
function copy_list(#[StdList(Type::Int)] $list): void
{
$list[] = 99;
}
function append_list(#[StdList(Type::Int)] &$list): void
{
$list[] = 42;
}
function main(): void
{
$list = std::list(Type::Int);
$list[-3] = 5;
$list[100] = 7;
$list[] = 8;
$copy = $list;
$alias =& $list;
$alias2 =& $alias;
$alias2[100] = 9;
copy_list($list);
append_list(list: $alias);
var_dump(is_array($list), count($list), $list[100], $copy[100]);
foreach ($list as $key => $value) {
echo $key, ':', $value, "\n";
}
$dict = std::dict(Type::Int, Type::Str);
$dict[-10] = 'minus';
$dict[1000] = 'sparse';
foreach ($dict as $id => $name) {
echo $id, ':', $name, "\n";
}
unset($list[100]);
var_dump(isset($list[100]), empty($list[100]), count($alias));
var_dump(array_search(8, $list), array_sum($list));
$visit = function() use ($list): int {
$list[] = 70;
return count($list);
};
var_dump($visit(), count($list));
}
?>
--EXPECT--
bool(true)
int(4)
int(9)
int(7)
-3:5
100:9
101:8
102:42
-10:minus
1000:sparse
bool(false)
bool(true)
int(3)
int(101)
int(55)
int(4)
int(3)
Loading…
Cancel
Save