From a3b5d61ff7eeba9182de0ef4fbedf82bb77e0424 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 17 Sep 2026 12:23:00 +0800 Subject: [PATCH] feat(compiler): add toStdList and toStdDict typed array conversion methods - Added toStdList and toStdDict methods to convert arrays to typed PHP arrays - Updated documentation to reflect 6 total Std container conversion methods - Implemented runtime conversion logic with strict key/value checking - Added performance warnings for O(n) array traversal operations - Enhanced method call parsing to handle new typed array conversion keywords - Preserved copy-on-write behavior for matching typed array contracts - Added support for ClassName::class as value type validation - Implemented scoped instance call handling for proper method resolution --- docs/en/COMPILE_TIME_FUNCTIONS.md | 6 ++-- docs/en/TYPED_ARRAYS.md | 6 ++++ docs/en/TYPE_ANNOTATIONS.md | 2 +- docs/zh-cn/COMPILE_TIME_FUNCTIONS.md | 6 ++-- docs/zh-cn/TYPED_ARRAYS.md | 6 ++++ docs/zh-cn/TYPE_ANNOTATIONS.md | 2 +- src/CompilerBase.php | 4 +++ src/Parser/MethodCallTrait.php | 29 +++++++++++++-- src/Parser/TypedArrayTrait.php | 54 ++++++++++++++++++++++++++++ 9 files changed, 107 insertions(+), 8 deletions(-) diff --git a/docs/en/COMPILE_TIME_FUNCTIONS.md b/docs/en/COMPILE_TIME_FUNCTIONS.md index 71c80b5b..e041f4aa 100644 --- a/docs/en/COMPILE_TIME_FUNCTIONS.md +++ b/docs/en/COMPILE_TIME_FUNCTIONS.md @@ -63,11 +63,11 @@ There are currently 17 `std::` compile-time entry points. | `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. +Lists and dicts retain PHP array storage and copy-on-write. 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 -There are currently 4 Std container conversion keyword methods. +There are currently 6 Std container conversion keyword methods. | Name | Purpose | Main limitation | | --- | --- | --- | @@ -75,6 +75,8 @@ There are currently 4 Std container conversion keyword methods. | `toStdVector(...)` | Wraps the variable as a std vector. | Can only be used in the top-level scope of the variable's first assignment. | | `toStdMap(...)` | Wraps the variable as a std map. | Can only be used in the top-level scope of the variable's first assignment. | | `toStdOrderedMap(...)` | Wraps the variable as a std ordered map. | Can only be used in the top-level scope of the variable's first assignment. | +| `toStdList($valueType)` | Converts to an integer-key typed PHP array. | An identical contract copies directly; other sources receive strict key and value checks. | +| `toStdDict($keyType, $valueType)` | Converts to a typed PHP dictionary. | Keys must be `Type::Int` or `Type::Str`; other rules match `toStdList()`. | ## Mechanisms not counted in this list diff --git a/docs/en/TYPED_ARRAYS.md b/docs/en/TYPED_ARRAYS.md index fd7f50bc..e26becec 100644 --- a/docs/en/TYPED_ARRAYS.md +++ b/docs/en/TYPED_ARRAYS.md @@ -24,6 +24,12 @@ 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. +`$source->toStdList(Type::Int)` and `$source->toStdDict(Type::Str, Type::Int)` convert values into new local typed arrays. A typed array with the same contract uses ordinary PHP array assignment. An ordinary array has every key and value checked strictly at runtime; other values first pass through `toArray()` and then receive the same checks. `ClassName::class` is accepted as a value type and checks that every value is an instance of that class. Conversion leaves the source array unchanged. + +**Performance:** Except for direct assignment from a typed array with the same contract, conversion traverses the entire array and checks every key and value, taking O(n) time. Non-array sources also run `toArray()` first. Repeated conversion of large arrays, especially inside loops, can be costly. Use these methods carefully; convert once at the typed boundary and reuse the result when possible. + +Validation uses the array's actual runtime key types. PHP normalizes numeric string keys such as `'123'` to integer keys, so an ordinary array with such a key fails the strict `toStdDict(Type::Str, ...)` check. A `StdDict` with the same contract is assigned directly and is not checked again. + 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. diff --git a/docs/en/TYPE_ANNOTATIONS.md b/docs/en/TYPE_ANNOTATIONS.md index cf3f5374..c66f89f6 100644 --- a/docs/en/TYPE_ANNOTATIONS.md +++ b/docs/en/TYPE_ANNOTATIONS.md @@ -93,7 +93,7 @@ Parameter entry validates the Box, container kind, leaf type, and full shape bef ## StdList / StdDict keys and values -Typed PHP arrays primarily establish their constraints statically. They introduce no runtime typed-array object and require no PHPX or HashTable changes. +Typed PHP arrays primarily establish their constraints statically. They introduce no runtime typed-array object and do not change PHP HashTable storage. Explicit `toStdList()` / `toStdDict()` conversion uses PHPX `toTypedArray()` to check each entry of the source array. - A list is an integer-key PHP array, allowing negative keys, sparse keys, and holes. It permits append. - A dict declares Int or Str keys and requires explicit keys, even for integer-key dicts. diff --git a/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md b/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md index 59ba311b..1f841e3d 100644 --- a/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md +++ b/docs/zh-cn/COMPILE_TIME_FUNCTIONS.md @@ -60,11 +60,11 @@ TypePHP 不再为编译器指令保留任何全局函数名。编译期 API 最 | `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)。 +list/dict 保留普通 PHP 数组存储和写时复制。值要求静态类型匹配,`any` / `var` 键插入内部严格检查;仅允许只读动态 PHP 数组调用,禁止 `std::ref()`。参数使用完全一致的 `StdList` / `StdDict` 类型注解,PHP 类型可省略或为 `array`,不允许 `mixed`;同类型的原生 `&` 参数可以修改调用方。详细示例及边界见[强类型 PHP 数组与类型注解](TYPED_ARRAYS.md)。 ## Std 容器转换关键词方法 -当前 Std 容器转换关键词方法共 4 个。 +当前 Std 容器转换关键词方法共 6 个。 | 名称 | 作用 | 主要限制 | | --- | --- | --- | @@ -72,6 +72,8 @@ list/dict 保留普通 PHP 数组存储和写时复制,不需要 `toStd*()`。 | `toStdVector(...)` | 将变量包装为 std vector。 | 只能在变量首次赋值的顶层作用域使用。 | | `toStdMap(...)` | 将变量包装为 std map。 | 只能在变量首次赋值的顶层作用域使用。 | | `toStdOrderedMap(...)` | 将变量包装为 std ordered map。 | 只能在变量首次赋值的顶层作用域使用。 | +| `toStdList($valueType)` | 转为整数键强类型 PHP 数组。 | 同契约来源直接赋值;其他来源逐项严格校验键和值。 | +| `toStdDict($keyType, $valueType)` | 转为强类型 PHP 字典。 | 键类型只能为 `Type::Int` 或 `Type::Str`;其他规则同 `toStdList()`。 | ## 不计入本文清单的机制 diff --git a/docs/zh-cn/TYPED_ARRAYS.md b/docs/zh-cn/TYPED_ARRAYS.md index 2917ddcc..45ef387d 100644 --- a/docs/zh-cn/TYPED_ARRAYS.md +++ b/docs/zh-cn/TYPED_ARRAYS.md @@ -21,6 +21,12 @@ function append(#[StdList(Type::Int)] array &$values): void PHP 类型可以省略或声明为兼容类型:`StdList` / `StdDict` 对应 `array`,`StdVector` / `StdMap` / `StdOrderedMap` 对应 `box`。不允许显式 `mixed`、`any`、可空类型、联合类型和其他不兼容类型。 +`$source->toStdList(Type::Int)` 和 `$source->toStdDict(Type::Str, Type::Int)` 可将值转换成新的局部强类型数组。同契约的强类型数组直接按 PHP 数组赋值;普通数组在运行时逐项严格校验键和值;其他值先经 `toArray()` 转为数组再校验。值类型也可写 `ClassName::class`,运行时要求每个值都是该类的实例。转换不会修改来源数组。 + +**性能提示:** 除同契约强类型数组的直接赋值外,转换会遍历整个数组,检查每个键和值,时间复杂度为 O(n)。非数组来源还要先执行 `toArray()`。大数组或循环中的反复转换可能明显增加耗时;应谨慎使用,尽量在数据进入强类型边界时转换一次,并复用结果。 + +校验依据数组在运行时实际保存的键类型。PHP 会把普通数组中的数字字符串键(如 `'123'`)规范化为整数键,因此这种数组不能通过 `toStdDict(Type::Str, ...)` 的严格校验;已是同契约 `StdDict` 的变量直接赋值,不会重新校验。 + list 支持负数、稀疏整数键和空洞,不做边界检查;只有 list 允许 `[]` 追加。dict 必须显式提供 int 或 str 键。动态 `any` / `var` 键插入内部严格检查,不做隐式转换。值要求静态类型匹配,`Type::Any` 值除外。 局部强类型数组禁止通过 `std::ref()`、元素引用、可修改或引用传递的数组函数逃逸到动态 PHP。类型一致的原生参数可以按引用传递。字符串键 dict 遍历时将 PHP 数字键恢复为字符串,不修改 phpx 或底层 HashTable。 diff --git a/docs/zh-cn/TYPE_ANNOTATIONS.md b/docs/zh-cn/TYPE_ANNOTATIONS.md index 5179dd75..22c2af4c 100644 --- a/docs/zh-cn/TYPE_ANNOTATIONS.md +++ b/docs/zh-cn/TYPE_ANNOTATIONS.md @@ -93,7 +93,7 @@ StdArray 参数入口校验 Box、容器种类、叶子类型和完整形状, ## StdList / StdDict 的键和值 -强类型 PHP 数组的约束主要在静态阶段建立,不增加运行时强类型数组对象,也不修改 PHPX 或 PHP HashTable。 +强类型 PHP 数组的约束主要在静态阶段建立,不增加运行时强类型数组对象,也不修改 PHP HashTable。显式 `toStdList()` / `toStdDict()` 转换使用 PHPX 的 `toTypedArray()` 逐项校验来源数组。 - list 是整数键 PHP 数组,允许负数、稀疏键和空洞,不是连续序列;支持 `[]` 追加。 - dict 的键只能声明为 Int 或 Str,必须显式提供键,包括整数键 dict 也不允许追加。 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 9f4ee461..88441d8e 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -178,6 +178,8 @@ class CompilerBase implements PropertyAccessContext 'toString' => Type::STR, 'toBool' => Type::BOOL, 'toArray' => Type::ARRAY, + 'toStdList' => Type::ARRAY, + 'toStdDict' => Type::ARRAY, 'toStream' => Type::STREAM, 'toBigInt' => Type::BIGINT, 'toBigFloat' => Type::BIGFLOAT, @@ -190,6 +192,8 @@ class CompilerBase implements PropertyAccessContext /** Keyword methods not listed here accept no arguments. */ public const array KEYWORD_METHOD_WITH_ARGUMENTS = [ 'toObject' => true, + 'toStdList' => true, + 'toStdDict' => true, ]; private const array STREAM_FUNCTIONS = [ diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 0366923b..66ad531a 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -610,6 +610,10 @@ trait MethodCallTrait if ($this->containsNullsafeChain($expr->var)) { return $this->parseNullsafeExpr($expr); } + if ($this->isNamedMethod($expr->name) + && in_array($expr->name->toString(), ['toStdList', 'toStdDict'], true)) { + return $this->parseTypedArrayConversionCall($expr); + } $class = ''; $materializedNativeReceiver = false; @@ -1200,7 +1204,12 @@ trait MethodCallTrait $calledCe = $this->getCalledCeExpr(); $direct = 'php::Var(' . self::PREFIX . $nativeFunc . '(this_))'; - $fallback = 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))'; + // Keep the receiver and qualify the runtime class: a child private + // method must not resolve as the parent's lexical private method. + $fallback = ($this->methodDef->flags & Modifiers::STATIC) + ? 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))' + : 'php::callScoped(this_, php::concat({typephp_get_called_class(' . $calledCe . ')' + . ', "::", ' . $methodPtr . '}), ' . $this->getCallableScopeExpr() . ')'; return '(EXPECTED(' . $calledCe . ' == ' . $this->getClassEntryPtr($class) . ')' . ' ? ' . $direct . ' : ' . $fallback . ')'; } @@ -1227,6 +1236,7 @@ trait MethodCallTrait $cacheCallable = false; $directStaticCall = false; $scopedStaticCall = false; + $scopedInstanceCall = false; $staticCallTarget = ''; $staticCallMethod = ''; $canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name); @@ -1291,7 +1301,11 @@ trait MethodCallTrait } $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $staticCallMethod . '})'; $placeHolder = $fn; - if ($staticCallTarget !== '') { + if ($class === 'static' && $this->methodDef !== null + && !($this->methodDef->flags & Modifiers::STATIC)) { + $scopedInstanceCall = true; + $fn = 'php::concat({' . $this->getCalledClassExpr() . ', "::", ' . $staticCallMethod . '})'; + } elseif ($staticCallTarget !== '') { $directStaticCall = true; } else { // `self::$method()` carries a lexical lookup class and a @@ -1325,6 +1339,10 @@ trait MethodCallTrait // Used to resolve the method signature when detecting by-reference arguments (late static binding is resolved within the current class hierarchy) $rtFunc = $method; $rtClass = $this->getFullClassName(); + if ($this->methodDef !== null && !($this->methodDef->flags & Modifiers::STATIC)) { + $scopedInstanceCall = true; + $fn = 'php::concat({' . $this->getCalledClassExpr() . ', "::", ' . $methodPtr . '})'; + } } else { if ($class === 'self') { $class = $this->getFullClassName(); @@ -1391,6 +1409,9 @@ trait MethodCallTrait } if (empty($expr->args)) { + if ($scopedInstanceCall) { + return 'php::callScoped(this_, ' . $fn . ', ' . $this->getCallableScopeExpr() . ')'; + } if ($scopedStaticCall) { return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ')'; } @@ -1403,6 +1424,10 @@ trait MethodCallTrait return 'php::call(' . $fn . ')'; } try { + if ($scopedInstanceCall) { + return 'php::callScoped(this_, ' . $fn . ', ' . $this->getCallableScopeExpr() . ', ' + . $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')'; + } if ($scopedStaticCall) { return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ', ' . $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')'; diff --git a/src/Parser/TypedArrayTrait.php b/src/Parser/TypedArrayTrait.php index 572e5c6c..1ea505cb 100644 --- a/src/Parser/TypedArrayTrait.php +++ b/src/Parser/TypedArrayTrait.php @@ -126,6 +126,16 @@ trait TypedArrayTrait if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) { return $this->getTypedArrayDefinition($expr->expr); } + if ($expr instanceof Expr\MethodCall && $expr->name instanceof Node\Identifier) { + $kind = match ($expr->name->name) { + 'toStdList' => 'list', + 'toStdDict' => 'dict', + default => null, + }; + if ($kind !== null) { + return $this->parseTypedArrayDefinition($kind, $expr->args, $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)) { @@ -137,6 +147,50 @@ trait TypedArrayTrait return null; } + protected function parseTypedArrayConversionCall(Expr\MethodCall $call): string + { + $kind = $call->name->toString() === 'toStdList' ? 'list' : 'dict'; + $definition = $this->parseTypedArrayDefinition($kind, $call->args, $call); + if ($this->isVarExpr($call->var)) { + $this->assertStdContainerDoesNotEscapeNativeObjects($call, $this->parseIdentifier($call->var)); + } + // A matching typed array already satisfies the contract. Preserve + // ordinary PHP array assignment and its copy-on-write behavior. + if ($this->getTypedArrayDefinition($call->var) === $definition) { + return $this->parseExprAsValue($call->var); + } + + $stdContainer = $this->isVarExpr($call->var) + && $this->isStdContainer($this->parseIdentifier($call->var)); + if ($this->detectTypeOfExpr($call->var) === Type::ARRAY && !$stdContainer) { + $source = $this->parseExprAsValue($call->var); + } else { + $class = $this->detectClassOfExpr($call->var); + if ($this->isNativeObjectClass($class)) { + // Native objects use their declared toArray() method; their + // pointer cannot be passed to PHPX's dynamic conversion. + $source = $this->parseExprAsValue(new Expr\MethodCall($call->var, new Node\Identifier('toArray'))); + } else { + $source = 'php::toArray(' . $this->parseExprAsValue($call->var) . ')'; + } + } + $valueType = match ($definition['type']) { + Type::INT => 'Int', + Type::FLOAT => 'Float', + Type::BOOL => 'Bool', + Type::STR => 'String', + Type::ARRAY => 'Array', + Type::OBJECT => 'Object', + Type::VAR => 'Any', + }; + $valueClass = $definition['class'] !== null && $definition['class'] !== '' + ? $this->getClassEntryPtr($definition['class']) + : 'nullptr'; + return 'php::toTypedArray(' . $source . ', ' + . ($definition['keyType'] === Type::STR ? 'true' : 'false') . ', ' + . 'php::TypedArrayValueType::' . $valueType . ', ' . $valueClass . ')'; + } + protected function getTypedArrayAccessDefinition(NodeAbstract $expr): ?array { return $expr instanceof Expr\ArrayDimFetch ? $this->getTypedArrayDefinition($expr->var) : null;