diff --git a/docs/en/STD_CONTAINERS.md b/docs/en/STD_CONTAINERS.md index cc57daae..a19c5f03 100644 --- a/docs/en/STD_CONTAINERS.md +++ b/docs/en/STD_CONTAINERS.md @@ -1,6 +1,6 @@ # Swoole AOT Strongly-Typed High-Performance Containers — Array Access Performance Improved by 10x -This document focuses on C++ containers. StdVector / StdMap / StdOrderedMap / StdList / StdDict are collectively type annotations. See [Unified Type Annotation Design](TYPE_ANNOTATIONS.md) for their contracts and storage boundaries, and [Typed PHP Arrays](TYPED_ARRAYS.md) for PHP-array list/dict storage. +This document focuses on C++ containers. StdArray / StdVector / StdMap / StdOrderedMap / StdList / StdDict are collectively type annotations. See [Unified Type Annotation Design](TYPE_ANNOTATIONS.md) for their contracts and storage boundaries, and [Typed PHP Arrays](TYPED_ARRAYS.md) for PHP-array list/dict storage. > Std Container uses a PHPX Box to hold a concrete C++ template instance. For its storage > and passing boundary relative to ordinary Zend Objects and Native diff --git a/docs/en/STD_FUNC_DESIGN.md b/docs/en/STD_FUNC_DESIGN.md index f2435809..d80b7676 100644 --- a/docs/en/STD_FUNC_DESIGN.md +++ b/docs/en/STD_FUNC_DESIGN.md @@ -1,6 +1,6 @@ # StdFunc / StdArgInfo Type Annotation Design -Status: an agreed design, not implemented. Names, flags, and syntax below describe the target interface, including the new StdArray annotation, not currently available features. See [unified type annotations](TYPE_ANNOTATIONS.md) for terminology and container contracts. +Status: the `StdFunc` / `StdArgInfo` design is not implemented; the referenced StdArray annotation is implemented. See [unified type annotations](TYPE_ANNOTATIONS.md) for terminology and container contracts. ## Goals and non-goals diff --git a/docs/en/TYPE_ANNOTATIONS.md b/docs/en/TYPE_ANNOTATIONS.md index 6ccb809a..886801ae 100644 --- a/docs/en/TYPE_ANNOTATIONS.md +++ b/docs/en/TYPE_ANNOTATIONS.md @@ -1,6 +1,6 @@ # Unified Type Annotation Design -Status: a combined record of existing container behavior and target design. The `StdArray` annotation, `StdFunc`, and `StdArgInfo` are not implemented. This document does not announce new available features. +Status: a combined record of existing container behavior and target design. The `StdArray` annotation is implemented; `StdFunc` and `StdArgInfo` are not. ## Terminology and responsibilities @@ -17,7 +17,7 @@ See [StdFunc and StdArgInfo](STD_FUNC_DESIGN.md), [typed PHP arrays](TYPED_ARRAY | Type annotation | Value factory | Storage and passing | Contract | |---|---|---|---| -| Proposed `StdArray(T, sizeOrDimensions)` | `std::array(T, N)` or existing nested factories | PHPX Box containing fixed-size C++ template instances | Leaf type, full shape, no holes | +| `StdArray(T, sizeOrDimensions)` | `std::array(T, N)` or existing nested factories | PHPX Box containing fixed-size C++ template instances | Leaf type, full shape, no holes | | `StdVector(T)` | `std::vector(T[, size])` | PHPX Box containing a C++ template instance | Contiguous integer indices and element type | | `StdMap(K, V)` | `std::map(K, V)` | PHPX Box containing a C++ hash map | Key and value types | | `StdOrderedMap(K, V)` | `std::orderedMap(K, V)` | PHPX Box containing a C++ ordered map | Key and value types | @@ -25,7 +25,7 @@ See [StdFunc and StdArgInfo](STD_FUNC_DESIGN.md), [typed PHP arrays](TYPED_ARRAY | `StdDict(K, V)` | `std::dict(K, V)` | Ordinary PHP array with COW | Key and value types, explicit keys required | | `StdFunc(R, args)` | Existing function or closure value | Proposed signature-bearing callable | Parameters, result, references, and omission rules | -The `std::array` container exists. Its `StdArray` annotation is a newly included target interface, not yet registered or integrated with parameter recovery, and must not be treated as available behavior. +Both the `std::array` container and its `StdArray` annotation are implemented. The annotation declares parameter/property contracts; it does not create values. Similar type arguments do not make storage models interchangeable. A Box is not a PHP array, and annotations do not change PHP's native type system. @@ -48,7 +48,7 @@ function append(#[StdList(Type::Int)] array &$items): void An annotation supplies the full contract. The PHP type may be omitted or name compatible storage: - `box` for `StdVector`, `StdMap`, and `StdOrderedMap`. -- An omitted PHP type or `box` for proposed `StdArray`, not PHP `array` storage. +- An omitted PHP type or `box` for `StdArray`, not PHP `array` storage. - `array` for `StdList` and `StdDict`. - A `callable` parameter for proposed `StdFunc`; nullable callback bindings require compatible nullable declarations, as specified in its design. @@ -70,7 +70,7 @@ A declaration has one primary type annotation; duplicates and conflicting contai **Special case: StdArray uses a different annotation form from other containers.** StdVector/StdList describe an element type, and StdMap/StdOrderedMap/StdDict describe key/value types. StdArray must describe both its leaf element type and fixed shape; the second argument is a length or dimension array, not a key type or another container type. Only StdArray supports structural nesting, so other containers' type-argument counts and parsing rules cannot be reused unchanged. Declaration checks, matching, caches, and stubs must retain dimensions. -Do not use recursive `new StdArray(new StdArray(...), ...)` descriptors or other container types as structural leaf types. Proposed declarations are: +Do not use recursive `new StdArray(new StdArray(...), ...)` descriptors or other container types as structural leaf types. Declarations are: ```php function process(#[StdArray(Type::Int, 100)] box $values): void {} @@ -81,13 +81,13 @@ The second argument is one length or a nonempty dimension array, ordered outermo - `StdArray(T, 100)` and `StdArray(T, [100])` normalize to the same type. - Retain the leaf type, resolved class, and outer-to-inner `dimensions`. Equality includes rank, order, and each length, not merely total element count. -- Dimensions are compile-time integer values, never dynamic variables or function calls. Negative dimensions are invalid; total element/byte calculations check overflow. Whether zero-length dimensions are permitted must explicitly align with fixed-container rules and be tested, rather than accidentally introduce different semantics in the annotation path. +- Dimensions are integer literals, never dynamic variables, constant expressions, or function calls. Negative dimensions are invalid; zero-length dimensions follow C++ `std::array`. Total element and estimated-byte calculations check overflow. - Only StdArray supports structural nesting. Multidimensional contracts lower to existing nested C++ `StdArray` storage. Existing nested `std::array(...)` value-factory syntax stays unchanged. - Each index consumes one dimension. For example, `$values[$i]` from this three-dimensional container has type `StdArray(T, [200, 8])`. -- Simpler syntax does not solve subarray ownership. Initially prefer independent copies for ordinary subarray assignment and parameter passing. Shared borrowing, reference assignment, and raw subarray pointers require separate lifetime design and are not automatically enabled. +- Simpler syntax does not solve subarray ownership. This implementation restores complete StdArray parameters only. Shared borrowing, subarray parameters, reference assignment, and raw subarray pointers require separate lifetime design and are not automatically enabled. - Default initialization recursively preserves the full shape. If Optional StdArray parameters are later enabled, their empty value is a correctly shaped default-initialized container, not ordinary `[]`; allocation, class-element initialization, and lifetime still need validation. -Annotation registration, Box entry checks/recovery, property metadata, subarray propagation, caches, and library stubs must retain the full shape. Existing nested factory support does not implement these parameter/property paths. +Parameter entry validates the Box, container kind, leaf type, and full shape before recovering the concrete C++ reference. Properties retain the same contract but still follow the existing property-read recovery boundary below. Declaration caches and library stubs retain dimensions. Independent subarray-parameter propagation is not enabled. ## StdList / StdDict keys and values diff --git a/docs/zh-cn/STD_CONTAINERS.md b/docs/zh-cn/STD_CONTAINERS.md index e778b13f..462eaa9f 100644 --- a/docs/zh-cn/STD_CONTAINERS.md +++ b/docs/zh-cn/STD_CONTAINERS.md @@ -1,6 +1,6 @@ # Swoole AOT 强类型高性能容器,数组访问性能提升 10 倍 -本文侧重 C++ 容器。`StdVector` / `StdMap` / `StdOrderedMap` / `StdList` / `StdDict` 统一称为“类型注解”,完整设计与不同存储边界见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。普通 PHP 数组模型的 list/dict 见 [强类型 PHP 数组](TYPED_ARRAYS.md)。 +本文侧重 C++ 容器。`StdArray` / `StdVector` / `StdMap` / `StdOrderedMap` / `StdList` / `StdDict` 统一称为“类型注解”,完整设计与不同存储边界见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。普通 PHP 数组模型的 list/dict 见 [强类型 PHP 数组](TYPED_ARRAYS.md)。 > Std Container 使用 PHPX Box 保存具体 C++ 模板实例。它与普通 Zend Object、Native > Class Object 的存储和传递边界见 diff --git a/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md b/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md index 5d88e2b9..98e740e7 100644 --- a/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md +++ b/docs/zh-cn/STD_CONTAINER_PARAMETER_ATTRIBUTES.md @@ -1,6 +1,6 @@ # Std 容器参数类型注解 -`StdVector`、`StdMap`、`StdOrderedMap` 是 TypePHP 内置的类型注解。 +`StdArray`、`StdVector`、`StdMap`、`StdOrderedMap` 是 TypePHP 内置的 Box 容器类型注解。 它们描述容器种类和元素类型,并自动完成以前需要手写的 `toStd*()` 类型恢复。 本文记录现有 Box 容器参数实现。包含 `StdList` / `StdDict` 的统一规则见 @@ -12,6 +12,11 @@ function append(#[StdVector(Type::Int)] $vec): void $vec[] = 42; } +function update_matrix(#[StdArray(Type::Int, [2, 3])] box $matrix): void +{ + $matrix[1][2] = 42; +} + function update(#[StdMap(Type::String, User::class)] $users): void { $users['alice'] = new User(); @@ -29,7 +34,7 @@ function visit(#[StdOrderedMap(Type::Int, Type::String)] $names): void - 一个参数只能有一个容器类型注解,不能重复或混用。 - 类型注解提供完整契约,PHP 参数类型可以省略或声明为兼容的 `box`;不能声明 `mixed`、`any`、`array`、Nullable、联合类型或其他不兼容类型。 -- `StdVector` 接受一个类型实参;`StdMap`、`StdOrderedMap` 接受 key/value 两个类型实参。 +- `StdArray` 接受叶子类型及单长度或外到内维度数组;`StdVector` 接受一个类型实参;`StdMap`、`StdOrderedMap` 接受 key/value 两个类型实参。 - 类型实参使用现有 std 工厂支持的 `Type::*` 或 `ClassName::class`;map 的 key 只支持 `Type::Int`、`Type::String`。 - 使用位置为具名函数和方法参数,包括接口、抽象方法和 trait 方法;支持 Attribute 别名导入。 - 首版不支持引用、可变、带默认值、构造器属性提升参数,以及 Closure、箭头函数和 Generator 参数。 diff --git a/docs/zh-cn/STD_FUNC_DESIGN.md b/docs/zh-cn/STD_FUNC_DESIGN.md index 28f669de..e8436c5b 100644 --- a/docs/zh-cn/STD_FUNC_DESIGN.md +++ b/docs/zh-cn/STD_FUNC_DESIGN.md @@ -1,6 +1,6 @@ # StdFunc / StdArgInfo 类型注解设计 -状态:已确认的方案,尚未实现。本文中的名称、标志和语法是目标接口,不是当前可用功能,包括新增的 StdArray 类型注解。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。 +状态:`StdFunc` / `StdArgInfo` 方案尚未实现;其中引用的 StdArray 类型注解已经实现。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。 ## 目标与非目标 diff --git a/docs/zh-cn/TYPE_ANNOTATIONS.md b/docs/zh-cn/TYPE_ANNOTATIONS.md index 73cf88b3..226fdafd 100644 --- a/docs/zh-cn/TYPE_ANNOTATIONS.md +++ b/docs/zh-cn/TYPE_ANNOTATIONS.md @@ -1,6 +1,6 @@ # 类型注解统一设计 -状态:现有容器实现与目标设计的统一记录;`StdArray` 类型注解及 `StdFunc` / `StdArgInfo` 尚未实现。本文不代表新增功能已经可用。 +状态:现有容器实现与目标设计的统一记录;`StdArray` 类型注解已经实现,`StdFunc` / `StdArgInfo` 尚未实现。 ## 术语与职责 @@ -17,7 +17,7 @@ | 类型注解 | 创建值 | 存储与传递 | 核心契约 | |---|---|---|---| -| `StdArray(T, sizeOrDimensions)`(拟议) | `std::array(T, N)` 或现有嵌套工厂 | PHPX Box,具体 C++ 定长模板实例 | 叶子类型、完整维度、禁止空洞 | +| `StdArray(T, sizeOrDimensions)` | `std::array(T, N)` 或现有嵌套工厂 | PHPX Box,具体 C++ 定长模板实例 | 叶子类型、完整维度、禁止空洞 | | `StdVector(T)` | `std::vector(T[, size])` | PHPX Box,具体 C++ 模板实例 | 连续整数索引、元素类型 | | `StdMap(K, V)` | `std::map(K, V)` | PHPX Box,C++ 哈希映射 | 键和值类型 | | `StdOrderedMap(K, V)` | `std::orderedMap(K, V)` | PHPX Box,C++ 有序映射 | 键和值类型 | @@ -25,7 +25,7 @@ | `StdDict(K, V)` | `std::dict(K, V)` | 普通 PHP array,保留 COW | 键和值类型、必须显式提供键 | | `StdFunc(R, args)` | 已有函数或闭包值 | 拟议的带签名 callable | 参数、返回值、引用与缺省调用规则 | -`std::array` 已实现,`StdArray` 类型注解是新纳入的目标接口,当前尚未注册或接入参数恢复,不能当作已可用功能。 +`std::array` 和 `StdArray` 类型注解均已实现。后者声明参数或属性契约,不创建容器值。 不同存储模型不能因为类型参数相似就互换。Box 不等于 PHP array;类型注解也不改变 PHP 原生类型系统。 @@ -48,7 +48,7 @@ function append(#[StdList(Type::Int)] array &$items): void 类型注解提供完整契约;PHP 类型可以省略,或者只声明兼容的存储类型: - `StdVector` / `StdMap` / `StdOrderedMap`:`box`。 -- 拟议 `StdArray`:省略 PHP 类型或声明为 `box`,不使用 PHP `array` 存储类型。 +- `StdArray`:省略 PHP 类型或声明为 `box`,不使用 PHP `array` 存储类型。 - `StdList` / `StdDict`:`array`。 - 拟议 `StdFunc`:`callable` 参数;回调本身可空时需要相容的 Nullable 声明,见专项设计。 @@ -70,7 +70,7 @@ function append(#[StdList(Type::Int)] array &$items): void **特殊性备注:StdArray 的类型注解方式与其他容器不同。** StdVector/StdList 只描述元素类型,StdMap/StdOrderedMap/StdDict 描述 key/value 类型;StdArray 必须同时描述叶子元素类型和固定形状,其第二个实参是长度或维度数组,不是 key 类型或另一个容器类型。只有 StdArray 支持结构性嵌套,因此不能直接复用其他容器的类型实参数量及解析规则;声明检查、类型匹配、缓存和 stub 均需保留维度信息。 -不使用递归 `new StdArray(new StdArray(...), ...)` 描述,也不允许以其他容器类型作为叶子类型。拟议声明为: +不使用递归 `new StdArray(new StdArray(...), ...)` 描述,也不允许以其他容器类型作为叶子类型。声明为: ```php function process(#[StdArray(Type::Int, 100)] box $values): void {} @@ -81,13 +81,13 @@ function matrix(#[StdArray(Type::Int, [100, 200, 8])] box $values): void {} - `StdArray(T, 100)` 与 `StdArray(T, [100])` 规范化为相同类型。 - 保存叶子类型、解析后的类名及统一外到内的 `dimensions`;类型比较包含维度数量、顺序和每层长度,不比较元素总数来判断相等。 -- 维度必须是编译期可确定的整数,不能使用动态变量或函数调用,不能为负数,计算总元素数和字节数须检查溢出。零长度维度是否允许需与现有定长容器规则明确对齐后验收,不能在新注解路径上意外获得不同语义。 +- 维度必须是编译期整数字面量,不能使用动态变量、常量表达式或函数调用,不能为负数;零长度维度与 C++ `std::array` 一致。编译器检查总元素数和估算字节数溢出。 - 只有 StdArray 支持结构性嵌套;多维类型降低为现有嵌套 C++ `StdArray`,不修改底层存储。现有创建值的嵌套 `std::array(...)` 工厂语法保持不变。 - 每消费一层索引,剩余维度组成子数组类型,例如三维容器的 `$values[$i]` 为 `StdArray(T, [200, 8])`。 -- 简化声明语法不消除子数组所有权问题。建议首阶段普通子数组赋值及作为参数传递采用独立复制;共享借用、引用赋值和子数组裸指针传递仍需独立生命周期设计,不因维度数组语法自动开放。 +- 简化声明语法不消除子数组所有权问题。本次实现只恢复完整 StdArray 参数;共享借用、子数组参数、引用赋值和子数组裸指针传递仍需独立生命周期设计,不因维度数组语法自动开放。 - 嵌套元素默认初始化必须递归保留完整形状。如果后续允许 Optional 的 StdArray,缺省值是该形状的默认初始化容器,不是普通 `[]`;创建、类元素初始化与生命周期仍需验证。 -StdArray 类型注解接入时还需在注册、Box 入口检查/恢复、属性元数据、子数组类型传导、缓存和 library stub 中保存完整维度。现有工厂支持嵌套,不代表这些参数/属性路径已实现。 +StdArray 参数入口校验 Box、容器种类、叶子类型和完整形状,然后恢复具体 C++ 引用;属性保存同一契约,但属性读取仍遵循下文的现有恢复边界。声明缓存和 library stub 保留维度。子数组作为独立参数的传导尚未开放。 ## StdList / StdDict 的键和值 diff --git a/phpunit/code/std-container-parameters.php b/phpunit/code/std-container-parameters.php index 173f640f..1a05f222 100644 --- a/phpunit/code/std-container-parameters.php +++ b/phpunit/code/std-container-parameters.php @@ -1,5 +1,6 @@ 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', 'StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict'], + ['Native', 'MethodsFor', 'NoExport', 'WasmExport', 'StdArray', 'StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict'], CompileTimeAttributeRegistry::namesForPhase(CompileTimeAttributeRegistry::PHASE_PREPROCESS), ); $this->assertSame( diff --git a/phpunit/src/StdAttributeTypeTest.php b/phpunit/src/StdAttributeTypeTest.php index 97efeb01..6260f95b 100644 --- a/phpunit/src/StdAttributeTypeTest.php +++ b/phpunit/src/StdAttributeTypeTest.php @@ -29,16 +29,25 @@ final class StdAttributeTypeTest extends BaseTest { [$code, $compiler] = $this->translate(<<<'PHP' function vector_arg(#[StdVector(Type::Int)] box $v): void { $v[] = 1; } +function array_arg(#[StdArray(Type::Int, [2, 3])] box $v): void { $v[1][2] = 1; } +function array_arg_scalar(#[StdArray(Type::Int, 3)] box $v): void { $v[2] = 1; } +function array_arg_vector(#[StdArray(Type::Int, [3])] box $v): void { $v[2] = 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_array_arg(php::Var v)', $code); + self::assertStringContainsString('php::StdArray, 2>', $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( + $getFunction->invoke($compiler, 'array_arg_scalar')->argInfoList[0]->stdContainer, + $getFunction->invoke($compiler, 'array_arg_vector')->argInfoList[0]->stdContainer, + ); 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); } @@ -53,6 +62,7 @@ class State { #[StdList(Type::Str)] public $inferred = []; #[StdDict(Type::Int, Type::Str)] public static array $labels = []; #[StdVector(Type::Int)] public box $vector; + #[StdArray(Type::Int, [2, 3])] public box $matrix; #[StdMap(Type::Str, Type::Int)] public box $map; #[StdOrderedMap(Type::Int, User::class)] public $ordered; } @@ -76,6 +86,8 @@ PHP); 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('array', $class->getProperty('matrix')->stdContainer['kind']); + self::assertSame([2, 3], $class->getProperty('matrix')->stdContainer['dimensions']); self::assertSame('ordered_map', $class->getProperty('ordered')->stdContainer['kind']); self::assertSame(Type::BOX, $class->getProperty('ordered')->type); } @@ -93,7 +105,7 @@ PHP); public static function conflictingDeclarations(): iterable { - foreach (['StdVector(Type::Int)', 'StdMap(Type::Str, Type::Int)', 'StdOrderedMap(Type::Int, Type::Str)'] as $attribute) { + foreach (['StdArray(Type::Int, 3)', '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']; diff --git a/phpunit/src/StdContainerParameterTest.php b/phpunit/src/StdContainerParameterTest.php index fe60e5d6..6d1d212d 100644 --- a/phpunit/src/StdContainerParameterTest.php +++ b/phpunit/src/StdContainerParameterTest.php @@ -15,6 +15,7 @@ final class StdContainerParameterTest extends BaseTest ); $code = $generator->generate([TYPEPHP_ROOT_PATH . '/phpunit/code/std-container-parameters.php'], []); self::assertStringContainsString('StdVector(', $code); + self::assertStringContainsString('StdArray(', $code); self::assertStringContainsString('StdMap(', $code); self::assertStringContainsString('StdOrderedMap(', $code); self::assertStringContainsString('Type::Int', $code); @@ -29,6 +30,7 @@ final class StdContainerParameterTest extends BaseTest $code = file_get_contents($translator->getCppFile($source)); self::assertStringContainsString('php_std_parameter_vector(php::Var vec)', $code); self::assertStringContainsString('auto &vec_ref = php::toStdContainer>(vec, ', $code); + self::assertStringContainsString('auto &matrix_ref = php::toStdContainer, 2>>(matrix, ', $code); self::assertStringContainsString('auto &map_ref = php::toStdContainer<', $code); self::assertStringContainsString('auto &users_ref = php::toStdContainer<', $code); self::assertStringNotContainsString('php::Var vec;', $code); @@ -39,6 +41,10 @@ final class StdContainerParameterTest extends BaseTest self::assertSame('vector', $parameter->stdContainer['kind']); self::assertArrayNotHasKey('typeId', $parameter->stdContainer); self::assertSame($parameter->stdContainer, unserialize(serialize($parameter))->stdContainer); + $matrix = (new ReflectionMethod($translator, 'getFunction'))->invoke($translator, 'std_parameter_matrix')->argInfoList[0]; + self::assertSame('array', $matrix->stdContainer['kind']); + self::assertSame([2, 3], $matrix->stdContainer['dimensions']); + self::assertSame([3, 2], $matrix->stdContainer['sizes']); } #[DataProvider('invalidDeclarations')] @@ -72,6 +78,11 @@ final class StdContainerParameterTest extends BaseTest yield 'two containers' => ['function foo(#[StdVector(Type::Int), StdMap(Type::Int, Type::Int)] $vec): void {}', 'cannot be applied to the same declaration']; yield 'missing type' => ['function foo(#[StdVector] $vec): void {}', 'expects 1 type argument']; yield 'vector size' => ['function foo(#[StdVector(Type::Int, 3)] $vec): void {}', 'expects 1 type argument']; + yield 'array missing dimensions' => ['function foo(#[StdArray(Type::Int)] $value): void {}', 'expects an element type']; + yield 'array empty dimensions' => ['function foo(#[StdArray(Type::Int, [])] $value): void {}', 'dimensions cannot be empty']; + yield 'array dynamic dimensions' => ['function foo(#[StdArray(Type::Int, SIZE)] $value): void {}', 'expects an integer size']; + yield 'array keyed dimensions' => ['function foo(#[StdArray(Type::Int, [0 => 2])] $value): void {}', 'positional array']; + yield 'array negative dimension' => ['function foo(#[StdArray(Type::Int, [-1])] $value): void {}', 'integer literals']; yield 'map missing value' => ['function foo(#[StdMap(Type::Int)] $vec): void {}', 'expects 2 type argument']; yield 'invalid key' => ['function foo(#[StdMap(Type::Float, Type::Int)] $vec): void {}', 'key only supports Type::Int or Type::String']; yield 'literal argument' => ['function foo(#[StdVector("int")] $vec): void {}', 'expects a Type constant']; @@ -84,6 +95,7 @@ final class StdContainerParameterTest extends BaseTest yield 'wrong target' => ['#[StdVector(Type::Int)] function foo(): void {}', 'can only be applied']; yield 'generator' => ['function foo(#[StdVector(Type::Int)] $vec) { yield 1; }', 'not supported on generators']; yield 'native elements' => ['#[Native] class User {} function foo(#[StdVector(User::class)] $vec): void {}', 'cannot hold Native objects']; + yield 'array native elements' => ['#[Native] class User {} function foo(#[StdArray(User::class, 2)] $value): void {}', 'cannot hold Native objects']; yield 'reference capture' => ['function foo(#[StdVector(Type::Int)] $vec): void { $fn = function() use (&$vec) {}; }', 'cannot be captured by reference']; yield 'unset binding' => ['function foo(#[StdVector(Type::Int)] $vec): void { unset($vec); }', 'bindings cannot be unset']; yield 'replace boxed binding' => ['function foo(#[StdVector(Type::Int)] $vec, $other): void { $vec = $other; }', 'bindings cannot be replaced']; diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 8ed653f0..4daaf5ef 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -43,7 +43,7 @@ trait CompilationStateTrait 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'])) { + if ($this->isStdContainerParameter($name)) { $this->fatalError(new Variable($sourceName), 'Std container parameters cannot be captured by reference'); } $type = $this->getRawVarType($name); diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index cff591fb..32f1b5d7 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -559,7 +559,7 @@ trait AssignOpTrait if ($copyAssign !== null) { return $copyAssign; } - if (!empty($this->context->stdContainers[$var]['parameter'])) { + if ($this->isStdContainerParameter($var)) { $this->fatalError($left, 'Std container parameter bindings cannot be replaced; modify the container contents instead'); } } diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 495aa206..29157990 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -1079,7 +1079,7 @@ trait PropertyAccessTrait $this->fatalError($var, 'Attempt to unset static property ' . $this->parseIdentifier($var->class) . '::$' . $this->parseIdentifier($var->name)); } elseif ($this->isVarExpr($var)) { $name = $this->parseIdentifier($var); - if (!empty($this->context->stdContainers[$name]['parameter'])) { + if ($this->isStdContainerParameter($name)) { $this->fatalError($var, 'Std container parameter bindings cannot be unset'); } if (!$this->hasVar($name)) { diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index 0b91e08f..33fa2d2e 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -42,15 +42,19 @@ trait StdContainerTrait protected function parseStdParameterDefinition(Node\Param|Node\Stmt\Property $param): ?array { + $arrayAttribute = CompileTimeAttribute::find($param, 'StdArray'); + if ($arrayAttribute !== null) { + $this->validateStdAttributeType($param, 'StdArray', 'box'); + $this->validateStdContainerParameterShape($param, 'StdArray'); + return $this->parseStdArrayAttributeDefinition($arrayAttribute); + } foreach (['StdVector' => 'vector', 'StdMap' => 'map', 'StdOrderedMap' => 'orderedMap'] as $name => $method) { $attribute = CompileTimeAttribute::find($param, $name); if ($attribute === null) { continue; } $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'); - } + $this->validateStdContainerParameterShape($param, $name); $expected = $method === 'vector' ? 1 : 2; if (count($attribute->args) !== $expected) { $this->fatalError($attribute, $name . ' expects ' . $expected . ' type argument(s)'); @@ -87,6 +91,84 @@ trait StdContainerTrait return null; } + protected function validateStdContainerParameterShape(Node\Param|Node\Stmt\Property $owner, string $name): void + { + if ($owner instanceof Node\Param + && ($owner->byRef || $owner->variadic || $owner->default !== null || $owner->isPromoted()) + ) { + $this->fatalError($owner, $name . ' does not support reference, variadic, defaulted or promoted parameters'); + } + } + + protected function parseStdArrayAttributeDefinition(Node\Attribute $attribute): array + { + if (count($attribute->args) !== 2) { + $this->fatalError($attribute, 'StdArray expects an element type and a size or dimensions array'); + } + foreach ($attribute->args as $argument) { + if ($argument->name !== null || $argument->unpack || $argument->byRef) { + $this->fatalError($argument, 'StdArray requires positional type and dimension arguments'); + } + } + + $typeInfo = $this->parseStdValueTypeInfo($attribute->args[0]->value, 'StdArray'); + if ($this->isNativeObjectClass($typeInfo['class'] ?? '')) { + $this->fatalError($attribute, 'StdArray parameters cannot hold Native objects across a Box boundary'); + } + $dimensions = $this->parseStdArrayAttributeDimensions($attribute->args[1]->value); + $totalElements = 1; + foreach ($dimensions as $dimension) { + if ($dimension !== 0 && $totalElements > intdiv(PHP_INT_MAX, $dimension)) { + $this->fatalError($attribute, 'StdArray dimensions are too large'); + } + $totalElements *= $dimension; + } + $bytesPerElement = $this->getStdValueTypeBytes($typeInfo['type']); + if ($totalElements !== 0 && $totalElements > intdiv(PHP_INT_MAX, $bytesPerElement)) { + $this->fatalError($attribute, 'StdArray dimensions exceed the supported storage size'); + } + + return [ + 'kind' => 'array', + 'decl' => $this->getStdArrayDecl($typeInfo['type'], $dimensions, $typeInfo['class']), + 'type' => $typeInfo['type'], + 'class' => $typeInfo['class'], + // Existing std::array lowering stores sizes inner-to-outer. Keep + // that representation while exposing the canonical declaration + // order explicitly for caches, diagnostics, and future consumers. + 'sizes' => array_reverse($dimensions), + 'dimensions' => $dimensions, + 'bytes' => $totalElements * $bytesPerElement, + ]; + } + + /** @return list */ + protected function parseStdArrayAttributeDimensions(NodeAbstract $expr): array + { + if ($this->isScalarInt($expr)) { + $dimensions = [$expr->value]; + } elseif ($expr instanceof Expr\Array_) { + if ($expr->items === []) { + $this->fatalError($expr, 'StdArray dimensions cannot be empty'); + } + $dimensions = []; + foreach ($expr->items as $item) { + if ($item === null || $item->key !== null || $item->unpack || !$this->isScalarInt($item->value)) { + $this->fatalError($item ?? $expr, 'StdArray dimensions must be a positional array of integer literals'); + } + $dimensions[] = $item->value->value; + } + } else { + $this->fatalError($expr, 'StdArray expects an integer size or a dimensions array'); + } + foreach ($dimensions as $dimension) { + if ($dimension < 0) { + $this->fatalError($expr, 'StdArray dimensions cannot be negative'); + } + } + return $dimensions; + } + protected function initializeStdContainerParameters(FunctionDef $function): string { $code = ''; @@ -96,12 +178,17 @@ trait StdContainerTrait } $info = $this->addStdTypeId($argument->stdContainer); $info['parameter'] = true; - $this->context->stdContainers[$argument->name] = $info; $type = match ($info['kind']) { + 'array' => Type::STD_ARRAY, 'vector' => Type::STD_VECTOR, 'map' => Type::STD_MAP, 'ordered_map' => Type::STD_ORDERED_MAP, }; + if ($type === Type::STD_ARRAY) { + $this->context->stdArrays[$argument->name] = $info; + } else { + $this->context->stdContainers[$argument->name] = $info; + } $this->addLocalVar($argument->name, $type); $code .= 'if (UNEXPECTED(!' . $argument->name . '.isBox())) { php::throwStdContainerTypeMismatch(); }' . PHP_EOL; $code .= 'auto &' . $argument->name . '_ref = php::toStdContainer<' . $info['decl'] . '>(' @@ -110,6 +197,12 @@ trait StdContainerTrait return $code; } + protected function isStdContainerParameter(string $name): bool + { + return !empty($this->context->stdContainers[$name]['parameter']) + || !empty($this->context->stdArrays[$name]['parameter']); + } + /** * Resolve the Native value class of a std container factory without * creating container metadata. This is used before assignment lowering so @@ -333,6 +426,7 @@ trait StdContainerTrait 'type' => $info['type'], 'class' => $info['class'], 'sizes' => array_reverse($nestedSizes), + 'dimensions' => $nestedSizes, 'bytes' => array_product($nestedSizes) * $this->getStdValueTypeBytes($info['type']), ]; } @@ -1005,6 +1099,7 @@ trait StdContainerTrait 'type' => $type, 'class' => $typeInfo['class'], 'sizes' => array_reverse($nesting), + 'dimensions' => $nesting, 'bytes' => $totalBytes, ]); return '// ' . $decl; diff --git a/src/Transform/CompileTimeAttributeRegistry.php b/src/Transform/CompileTimeAttributeRegistry.php index 4af54f80..0e58670a 100644 --- a/src/Transform/CompileTimeAttributeRegistry.php +++ b/src/Transform/CompileTimeAttributeRegistry.php @@ -100,7 +100,7 @@ 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); - $containerAttributes = ['StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict']; + $containerAttributes = ['StdArray', 'StdVector', 'StdMap', 'StdOrderedMap', 'StdList', 'StdDict']; foreach ($containerAttributes as $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]))); } diff --git a/src/polyfills.php b/src/polyfills.php index a415bb75..3d9afc66 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -115,6 +115,12 @@ final readonly class Constructor { } +#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)] +final readonly class StdArray +{ + public function __construct(string $valueType, int|array $sizeOrDimensions) {} +} + #[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)] final readonly class StdVector { diff --git a/tests/compiler/std-array/parameter-attributes.phpt b/tests/compiler/std-array/parameter-attributes.phpt new file mode 100644 index 00000000..3473a776 --- /dev/null +++ b/tests/compiler/std-array/parameter-attributes.phpt @@ -0,0 +1,62 @@ +--TEST-- +StdArray parameter annotations restore checked fixed-shape containers +--FILE-- +id, "\n"; +} + +function main(): void +{ + $matrix = std::array(std::array(Type::Int, 3), 2); + update_matrix($matrix); + var_dump($matrix[1][2]); + + $line = std::array(Type::Int, 2); + update_line($line); + var_dump($line[1]); + + $users = std::array(ArrayAnnotationUser::class, 2); + $users[1] = new ArrayAnnotationUser(7); + read_user($users); + + $wrongShape = std::array(std::array(Type::Int, 2), 3); + try { + update_matrix($wrongShape); + } catch (TypeError $error) { + echo "wrong shape\n"; + } + + try { + update_matrix([[], []]); + } catch (TypeError $error) { + echo "not a container\n"; + } +} +?> +--EXPECT-- +2:3:42 +int(42) +int(9) +7 +wrong shape +not a container