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
master
韩天峰 3 weeks ago
parent 7dcdd959d4
commit a3b5d61ff7
  1. 6
      docs/en/COMPILE_TIME_FUNCTIONS.md
  2. 6
      docs/en/TYPED_ARRAYS.md
  3. 2
      docs/en/TYPE_ANNOTATIONS.md
  4. 6
      docs/zh-cn/COMPILE_TIME_FUNCTIONS.md
  5. 6
      docs/zh-cn/TYPED_ARRAYS.md
  6. 2
      docs/zh-cn/TYPE_ANNOTATIONS.md
  7. 4
      src/CompilerBase.php
  8. 29
      src/Parser/MethodCallTrait.php
  9. 54
      src/Parser/TypedArrayTrait.php

@ -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

@ -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.

@ -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.

@ -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()`。 |
## 不计入本文清单的机制

@ -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。

@ -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 也不允许追加。

@ -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 = [

@ -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) . ')';

@ -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;

Loading…
Cancel
Save