From f3f686ec46a6e253c764475db859bbbeaa5e3752 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 16 Sep 2026 10:25:49 +0800 Subject: [PATCH] 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 --- README-CN.md | 4 +- README.md | 4 +- docs/en/ARRAY_DEF.md | 42 -- docs/en/COMPILE_TIME_FUNCTIONS.md | 6 +- docs/en/README.md | 2 +- docs/en/TYPED_ARRAYS.md | 36 ++ docs/zh-cn/COMPILE_TIME_FUNCTIONS.md | 6 +- docs/zh-cn/README.md | 2 +- docs/zh-cn/TYPED_ARRAYS.md | 24 + phpunit/code/array-def-class-map-key.php | 7 - .../code/array-def-inclusive-upper-bound.php | 13 - phpunit/code/array-def-invalid-map-key.php | 6 - phpunit/code/array-def-map-append.php | 10 - phpunit/code/array-def-native-class-value.php | 8 - phpunit/code/array-def-no-arguments.php | 6 - phpunit/code/array-def-non-array-property.php | 6 - .../code/array-def-static-class-mismatch.php | 12 - .../code/array-def-static-key-mismatch.php | 10 - .../code/array-def-static-value-mismatch.php | 11 - .../code/array-def-std-container-value.php | 11 - phpunit/code/array-def-too-many-arguments.php | 6 - .../typed-array-property-class-map-key.php | 7 + ...d-array-property-inclusive-upper-bound.php | 13 + .../typed-array-property-invalid-map-key.php | 6 + .../code/typed-array-property-map-append.php | 10 + ...yped-array-property-native-class-value.php | 8 + .../typed-array-property-no-arguments.php | 6 + ...yped-array-property-non-array-property.php | 6 + ...d-array-property-static-class-mismatch.php | 12 + ...ped-array-property-static-key-mismatch.php | 10 + ...d-array-property-static-value-mismatch.php | 11 + ...ped-array-property-std-container-value.php | 11 + ...yped-array-property-too-many-arguments.php | 6 + phpunit/src/ArrayDefTest.php | 39 -- .../src/CompileTimeAttributeRegistryTest.php | 4 +- phpunit/src/StdAttributeTypeTest.php | 113 +++++ phpunit/src/TypedArrayPropertyTest.php | 39 ++ phpunit/src/TypedArrayTest.php | 169 +++++++ src/ArrayDef/ArrayDefSupportTrait.php | 255 ----------- src/ArrayDef/ArrayDefWritePlan.php | 19 - src/ArrayDef/ArrayDefinition.php | 23 - src/CompilerBase.php | 24 +- src/Context/CompilationStateTrait.php | 3 + src/Context/FunctionContext.php | 2 + src/Entity/ArgInfo.php | 2 + src/Entity/PropertyDef.php | 4 +- src/Entity/TypedArrayPropertyWritePlan.php | 13 + src/Generator/CallArgumentGenerator.php | 5 + src/Generator/ClosureGenerator.php | 19 +- src/Optimizer/FuncCallOptimizer.php | 2 +- src/Parser/ArrayExpressionTrait.php | 6 + src/Parser/AssignOpTrait.php | 58 ++- src/Parser/ForeachTrait.php | 4 + src/Parser/FunctionCallTrait.php | 8 +- src/Parser/PropertyAccessTrait.php | 2 + src/Parser/StdContainerTrait.php | 28 +- src/Parser/TypeConversionTrait.php | 1 + src/Parser/TypedArrayTrait.php | 419 ++++++++++++++++++ src/Parser/UniversalMethodCall.php | 21 + src/Preprocessor.php | 28 +- src/Transform/CompileTimeAttribute.php | 2 +- .../CompileTimeAttributeRegistry.php | 6 +- src/Translator.php | 30 +- src/polyfills.php | 37 +- tests/compiler/array-def/compound-types.phpt | 84 ---- .../attributes-and-properties.phpt | 71 +++ tests/compiler/typed-array/dynamic-keys.phpt | 57 +++ .../typed-array/exact-and-readonly.phpt | 46 ++ tests/compiler/typed-array/objects.phpt | 55 +++ .../property-classes.phpt} | 22 +- .../property-list-holes.phpt} | 29 +- .../typed-array/property-php-values.phpt | 32 ++ .../property-zend-and-native.phpt} | 55 +-- tests/compiler/typed-array/storage.phpt | 62 +++ 74 files changed, 1530 insertions(+), 706 deletions(-) delete mode 100644 docs/en/ARRAY_DEF.md create mode 100644 docs/en/TYPED_ARRAYS.md create mode 100644 docs/zh-cn/TYPED_ARRAYS.md delete mode 100644 phpunit/code/array-def-class-map-key.php delete mode 100644 phpunit/code/array-def-inclusive-upper-bound.php delete mode 100644 phpunit/code/array-def-invalid-map-key.php delete mode 100644 phpunit/code/array-def-map-append.php delete mode 100644 phpunit/code/array-def-native-class-value.php delete mode 100644 phpunit/code/array-def-no-arguments.php delete mode 100644 phpunit/code/array-def-non-array-property.php delete mode 100644 phpunit/code/array-def-static-class-mismatch.php delete mode 100644 phpunit/code/array-def-static-key-mismatch.php delete mode 100644 phpunit/code/array-def-static-value-mismatch.php delete mode 100644 phpunit/code/array-def-std-container-value.php delete mode 100644 phpunit/code/array-def-too-many-arguments.php create mode 100644 phpunit/code/typed-array-property-class-map-key.php create mode 100644 phpunit/code/typed-array-property-inclusive-upper-bound.php create mode 100644 phpunit/code/typed-array-property-invalid-map-key.php create mode 100644 phpunit/code/typed-array-property-map-append.php create mode 100644 phpunit/code/typed-array-property-native-class-value.php create mode 100644 phpunit/code/typed-array-property-no-arguments.php create mode 100644 phpunit/code/typed-array-property-non-array-property.php create mode 100644 phpunit/code/typed-array-property-static-class-mismatch.php create mode 100644 phpunit/code/typed-array-property-static-key-mismatch.php create mode 100644 phpunit/code/typed-array-property-static-value-mismatch.php create mode 100644 phpunit/code/typed-array-property-std-container-value.php create mode 100644 phpunit/code/typed-array-property-too-many-arguments.php delete mode 100644 phpunit/src/ArrayDefTest.php create mode 100644 phpunit/src/StdAttributeTypeTest.php create mode 100644 phpunit/src/TypedArrayPropertyTest.php create mode 100644 phpunit/src/TypedArrayTest.php delete mode 100644 src/ArrayDef/ArrayDefSupportTrait.php delete mode 100644 src/ArrayDef/ArrayDefWritePlan.php delete mode 100644 src/ArrayDef/ArrayDefinition.php create mode 100644 src/Entity/TypedArrayPropertyWritePlan.php create mode 100644 src/Parser/TypedArrayTrait.php delete mode 100644 tests/compiler/array-def/compound-types.phpt create mode 100644 tests/compiler/typed-array/attributes-and-properties.phpt create mode 100644 tests/compiler/typed-array/dynamic-keys.phpt create mode 100644 tests/compiler/typed-array/exact-and-readonly.phpt create mode 100644 tests/compiler/typed-array/objects.phpt rename tests/compiler/{array-def/class-values.phpt => typed-array/property-classes.phpt} (62%) rename tests/compiler/{array-def/list-unset-holes.phpt => typed-array/property-list-holes.phpt} (58%) create mode 100644 tests/compiler/typed-array/property-php-values.phpt rename tests/compiler/{array-def/zend-and-native.phpt => typed-array/property-zend-and-native.phpt} (52%) create mode 100644 tests/compiler/typed-array/storage.phpt diff --git a/README-CN.md b/README-CN.md index 627052ed..a7efaff2 100644 --- a/README-CN.md +++ b/README-CN.md @@ -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) —— 生成器降级与生命周期 diff --git a/README.md b/README.md index 8a33e4a6..b78f5bbd 100644 --- a/README.md +++ b/README.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 diff --git a/docs/en/ARRAY_DEF.md b/docs/en/ARRAY_DEF.md deleted file mode 100644 index dd1d7991..00000000 --- a/docs/en/ARRAY_DEF.md +++ /dev/null @@ -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 - - #[ArrayDef(Type::Int, Type::String)] - public array $labels = []; // map -} -``` - -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. diff --git a/docs/en/COMPILE_TIME_FUNCTIONS.md b/docs/en/COMPILE_TIME_FUNCTIONS.md index 4d8a8c6d..71c80b5b 100644 --- a/docs/en/COMPILE_TIME_FUNCTIONS.md +++ b/docs/en/COMPILE_TIME_FUNCTIONS.md @@ -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 diff --git a/docs/en/README.md b/docs/en/README.md index e463f994..1cee2443 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -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 diff --git a/docs/en/TYPED_ARRAYS.md b/docs/en/TYPED_ARRAYS.md new file mode 100644 index 00000000..21c422c9 --- /dev/null +++ b/docs/en/TYPED_ARRAYS.md @@ -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. diff --git a/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md b/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md index a993a387..59ba311b 100644 --- a/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md +++ b/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md @@ -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 容器转换关键词方法 diff --git a/docs/zh-cn/README.md b/docs/zh-cn/README.md index a8ee60fc..7de51dc6 100644 --- a/docs/zh-cn/README.md +++ b/docs/zh-cn/README.md @@ -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)。 ## 架构与维护 diff --git a/docs/zh-cn/TYPED_ARRAYS.md b/docs/zh-cn/TYPED_ARRAYS.md new file mode 100644 index 00000000..3528a2b4 --- /dev/null +++ b/docs/zh-cn/TYPED_ARRAYS.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 对象修改或对象/属性引用逃逸。属性读取不会自动升级为受封闭契约保护的局部强类型数组。 diff --git a/phpunit/code/array-def-class-map-key.php b/phpunit/code/array-def-class-map-key.php deleted file mode 100644 index 747b683c..00000000 --- a/phpunit/code/array-def-class-map-key.php +++ /dev/null @@ -1,7 +0,0 @@ -values[$index] = 'indexed'; - $box->values[count($box->values)] = 'counted'; -} diff --git a/phpunit/code/array-def-invalid-map-key.php b/phpunit/code/array-def-invalid-map-key.php deleted file mode 100644 index 4cf4a751..00000000 --- a/phpunit/code/array-def-invalid-map-key.php +++ /dev/null @@ -1,6 +0,0 @@ -value[] = 'bad'; -} diff --git a/phpunit/code/array-def-native-class-value.php b/phpunit/code/array-def-native-class-value.php deleted file mode 100644 index 91b7be6b..00000000 --- a/phpunit/code/array-def-native-class-value.php +++ /dev/null @@ -1,8 +0,0 @@ -users[] = new ArrayDefOtherUser(); -} diff --git a/phpunit/code/array-def-static-key-mismatch.php b/phpunit/code/array-def-static-key-mismatch.php deleted file mode 100644 index 9cc44dd1..00000000 --- a/phpunit/code/array-def-static-key-mismatch.php +++ /dev/null @@ -1,10 +0,0 @@ -value['bad'] = 'value'; -} diff --git a/phpunit/code/array-def-static-value-mismatch.php b/phpunit/code/array-def-static-value-mismatch.php deleted file mode 100644 index 1c434e85..00000000 --- a/phpunit/code/array-def-static-value-mismatch.php +++ /dev/null @@ -1,11 +0,0 @@ -value[] = 123; -} diff --git a/phpunit/code/array-def-std-container-value.php b/phpunit/code/array-def-std-container-value.php deleted file mode 100644 index 208fd257..00000000 --- a/phpunit/code/array-def-std-container-value.php +++ /dev/null @@ -1,11 +0,0 @@ -values[] = $values; -} diff --git a/phpunit/code/array-def-too-many-arguments.php b/phpunit/code/array-def-too-many-arguments.php deleted file mode 100644 index 153b6ed4..00000000 --- a/phpunit/code/array-def-too-many-arguments.php +++ /dev/null @@ -1,6 +0,0 @@ -values[$index] = 'indexed'; + $box->values[count($box->values)] = 'counted'; +} diff --git a/phpunit/code/typed-array-property-invalid-map-key.php b/phpunit/code/typed-array-property-invalid-map-key.php new file mode 100644 index 00000000..53daa0db --- /dev/null +++ b/phpunit/code/typed-array-property-invalid-map-key.php @@ -0,0 +1,6 @@ +value[] = 'bad'; +} diff --git a/phpunit/code/typed-array-property-native-class-value.php b/phpunit/code/typed-array-property-native-class-value.php new file mode 100644 index 00000000..30d85844 --- /dev/null +++ b/phpunit/code/typed-array-property-native-class-value.php @@ -0,0 +1,8 @@ +users[] = new TypedArrayPropertyOtherUser(); +} diff --git a/phpunit/code/typed-array-property-static-key-mismatch.php b/phpunit/code/typed-array-property-static-key-mismatch.php new file mode 100644 index 00000000..af2363e4 --- /dev/null +++ b/phpunit/code/typed-array-property-static-key-mismatch.php @@ -0,0 +1,10 @@ +value['bad'] = 'value'; +} diff --git a/phpunit/code/typed-array-property-static-value-mismatch.php b/phpunit/code/typed-array-property-static-value-mismatch.php new file mode 100644 index 00000000..c05ecb57 --- /dev/null +++ b/phpunit/code/typed-array-property-static-value-mismatch.php @@ -0,0 +1,11 @@ +value[] = 123; +} diff --git a/phpunit/code/typed-array-property-std-container-value.php b/phpunit/code/typed-array-property-std-container-value.php new file mode 100644 index 00000000..d5a13d16 --- /dev/null +++ b/phpunit/code/typed-array-property-std-container-value.php @@ -0,0 +1,11 @@ +values[] = $values; +} diff --git a/phpunit/code/typed-array-property-too-many-arguments.php b/phpunit/code/typed-array-property-too-many-arguments.php new file mode 100644 index 00000000..af8311dc --- /dev/null +++ b/phpunit/code/typed-array-property-too-many-arguments.php @@ -0,0 +1,6 @@ +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); - } -} diff --git a/phpunit/src/CompileTimeAttributeRegistryTest.php b/phpunit/src/CompileTimeAttributeRegistryTest.php index cb7c7eba..5856d8bc 100644 --- a/phpunit/src/CompileTimeAttributeRegistryTest.php +++ b/phpunit/src/CompileTimeAttributeRegistryTest.php @@ -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( diff --git a/phpunit/src/StdAttributeTypeTest.php b/phpunit/src/StdAttributeTypeTest.php new file mode 100644 index 00000000..97efeb01 --- /dev/null +++ b/phpunit/src/StdAttributeTypeTest.php @@ -0,0 +1,113 @@ +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']; + } +} diff --git a/phpunit/src/TypedArrayPropertyTest.php b/phpunit/src/TypedArrayPropertyTest.php new file mode 100644 index 00000000..3f1e11ec --- /dev/null +++ b/phpunit/src/TypedArrayPropertyTest.php @@ -0,0 +1,39 @@ +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); + } +} diff --git a/phpunit/src/TypedArrayTest.php b/phpunit/src/TypedArrayTest.php new file mode 100644 index 00000000..fe7c0d09 --- /dev/null +++ b/phpunit/src/TypedArrayTest.php @@ -0,0 +1,169 @@ +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']; + } +} diff --git a/src/ArrayDef/ArrayDefSupportTrait.php b/src/ArrayDef/ArrayDefSupportTrait.php deleted file mode 100644 index b24030a3..00000000 --- a/src/ArrayDef/ArrayDefSupportTrait.php +++ /dev/null @@ -1,255 +0,0 @@ -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', - Type::BIGFLOAT => 'php::toBoxExact', - Type::DECIMAL => 'php::toBoxExact', - 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); - } - -} diff --git a/src/ArrayDef/ArrayDefWritePlan.php b/src/ArrayDef/ArrayDefWritePlan.php deleted file mode 100644 index 80b14fad..00000000 --- a/src/ArrayDef/ArrayDefWritePlan.php +++ /dev/null @@ -1,19 +0,0 @@ -keyType === null; - } -} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0e636ec1..9f4ee461 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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)) ) { diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index be552c97..8ed653f0 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -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'); } diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 0ed55422..3040f9cf 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -75,6 +75,8 @@ class FunctionContext * @var array */ public array $stdContainers = []; + /** @var array */ + public array $typedArrays = []; public array $localVars = []; /** @var array Local variable => forced fallback storage type. */ public array $varTypeDegradations = []; diff --git a/src/Entity/ArgInfo.php b/src/Entity/ArgInfo.php index bee9389d..fe8c236a 100644 --- a/src/Entity/ArgInfo.php +++ b/src/Entity/ArgInfo.php @@ -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; diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index abfcc4f0..52949792 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -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; diff --git a/src/Entity/TypedArrayPropertyWritePlan.php b/src/Entity/TypedArrayPropertyWritePlan.php new file mode 100644 index 00000000..c5a06ed3 --- /dev/null +++ b/src/Entity/TypedArrayPropertyWritePlan.php @@ -0,0 +1,13 @@ + [$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; } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 01d5085b..342ff03b 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -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 $uses * @return array{ * cpp: list, - * bindings: list + * bindings: list * }|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])) { diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index c7a0b925..bce35f93 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -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'; } diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 837f179c..27a63f29 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -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'); diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 8a69b583..cff591fb 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -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); diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index be2a22f1..41bb101b 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -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)) { diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index fc20b09f..2ed677cb 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -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; diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index eeb2c900..495aa206 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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 { diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index 5110b2e3..0b91e08f 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -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"); diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 2b1d3a7b..373c4082 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -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) { diff --git a/src/Parser/TypedArrayTrait.php b/src/Parser/TypedArrayTrait.php new file mode 100644 index 00000000..dddafbcd --- /dev/null +++ b/src/Parser/TypedArrayTrait.php @@ -0,0 +1,419 @@ + '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); + } +} diff --git a/src/Parser/UniversalMethodCall.php b/src/Parser/UniversalMethodCall.php index fd830982..d620dfc1 100644 --- a/src/Parser/UniversalMethodCall.php +++ b/src/Parser/UniversalMethodCall.php @@ -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)', diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 24f0340e..af5f4e5c 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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' diff --git a/src/Transform/CompileTimeAttribute.php b/src/Transform/CompileTimeAttribute.php index aa5aa6ac..68a7f88c 100644 --- a/src/Transform/CompileTimeAttribute.php +++ b/src/Transform/CompileTimeAttribute.php @@ -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( diff --git a/src/Transform/CompileTimeAttributeRegistry.php b/src/Transform/CompileTimeAttributeRegistry.php index 8d21dc66..4af54f80 100644 --- a/src/Transform/CompileTimeAttributeRegistry.php +++ b/src/Transform/CompileTimeAttributeRegistry.php @@ -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; diff --git a/src/Translator.php b/src/Translator.php index be7e67be..fb6a99db 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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); } } diff --git a/src/polyfills.php b/src/polyfills.php index 91b9a7b5..a415bb75 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -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 []; + } } diff --git a/tests/compiler/array-def/compound-types.phpt b/tests/compiler/array-def/compound-types.phpt deleted file mode 100644 index c095c5a8..00000000 --- a/tests/compiler/array-def/compound-types.phpt +++ /dev/null @@ -1,84 +0,0 @@ ---TEST-- -ArrayDef checks dynamic compound and high-precision value types ---FILE-- -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 diff --git a/tests/compiler/typed-array/attributes-and-properties.phpt b/tests/compiler/typed-array/attributes-and-properties.phpt new file mode 100644 index 00000000..59a89d62 --- /dev/null +++ b/tests/compiler/typed-array/attributes-and-properties.phpt @@ -0,0 +1,71 @@ +--TEST-- +Std attributes accept matching array/box storage types and sparse property list writes +--FILE-- +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 diff --git a/tests/compiler/typed-array/dynamic-keys.phpt b/tests/compiler/typed-array/dynamic-keys.phpt new file mode 100644 index 00000000..2b753da4 --- /dev/null +++ b/tests/compiler/typed-array/dynamic-keys.phpt @@ -0,0 +1,57 @@ +--TEST-- +Typed PHP arrays strictly check dynamic keys without changing PHPX or coercing keys +--FILE-- +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) diff --git a/tests/compiler/typed-array/exact-and-readonly.phpt b/tests/compiler/typed-array/exact-and-readonly.phpt new file mode 100644 index 00000000..7fb1ef68 --- /dev/null +++ b/tests/compiler/typed-array/exact-and-readonly.phpt @@ -0,0 +1,46 @@ +--TEST-- +Typed array explicit key conversions, readonly calls and dynamic by-value COW isolation +--FILE-- +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) diff --git a/tests/compiler/typed-array/objects.phpt b/tests/compiler/typed-array/objects.phpt new file mode 100644 index 00000000..94426ab7 --- /dev/null +++ b/tests/compiler/typed-array/objects.phpt @@ -0,0 +1,55 @@ +--TEST-- +Typed list/dict class values propagate through foreach and native method parameters +--FILE-- + $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) diff --git a/tests/compiler/array-def/class-values.phpt b/tests/compiler/typed-array/property-classes.phpt similarity index 62% rename from tests/compiler/array-def/class-values.phpt rename to tests/compiler/typed-array/property-classes.phpt index 3901c9a3..91b4983d 100644 --- a/tests/compiler/array-def/class-values.phpt +++ b/tests/compiler/typed-array/property-classes.phpt @@ -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-- 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 diff --git a/tests/compiler/array-def/list-unset-holes.phpt b/tests/compiler/typed-array/property-list-holes.phpt similarity index 58% rename from tests/compiler/array-def/list-unset-holes.phpt rename to tests/compiler/typed-array/property-list-holes.phpt index f3a5d76f..40c3cbf3 100644 --- a/tests/compiler/array-def/list-unset-holes.phpt +++ b/tests/compiler/typed-array/property-list-holes.phpt @@ -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-- 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) diff --git a/tests/compiler/typed-array/property-php-values.phpt b/tests/compiler/typed-array/property-php-values.phpt new file mode 100644 index 00000000..6a69fe25 --- /dev/null +++ b/tests/compiler/typed-array/property-php-values.phpt @@ -0,0 +1,32 @@ +--TEST-- +StdList and StdDict properties support statically typed PHP array/object/any values +--FILE-- +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" diff --git a/tests/compiler/array-def/zend-and-native.phpt b/tests/compiler/typed-array/property-zend-and-native.phpt similarity index 52% rename from tests/compiler/array-def/zend-and-native.phpt rename to tests/compiler/typed-array/property-zend-and-native.phpt index 629e60ef..5adc7b70 100644 --- a/tests/compiler/array-def/zend-and-native.phpt +++ b/tests/compiler/typed-array/property-zend-and-native.phpt @@ -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-- 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" diff --git a/tests/compiler/typed-array/storage.phpt b/tests/compiler/typed-array/storage.phpt new file mode 100644 index 00000000..b951b2a5 --- /dev/null +++ b/tests/compiler/typed-array/storage.phpt @@ -0,0 +1,62 @@ +--TEST-- +Typed PHP arrays retain COW storage, native references, sparse keys and foreach types +--FILE-- + $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)