Skip to content

Commit a3b5d61

Browse files
committed
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
1 parent 7dcdd95 commit a3b5d61

9 files changed

Lines changed: 107 additions & 8 deletions

docs/en/COMPILE_TIME_FUNCTIONS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,20 @@ There are currently 17 `std::` compile-time entry points.
6363
| `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. |
6464
| `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. |
6565

66-
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.
66+
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.
6767

6868
## Std container conversion keyword methods
6969

70-
There are currently 4 Std container conversion keyword methods.
70+
There are currently 6 Std container conversion keyword methods.
7171

7272
| Name | Purpose | Main limitation |
7373
| --- | --- | --- |
7474
| `toStdArray(...)` | Wraps the variable as a std array. | Can only be used in the top-level scope of the variable's first assignment. |
7575
| `toStdVector(...)` | Wraps the variable as a std vector. | Can only be used in the top-level scope of the variable's first assignment. |
7676
| `toStdMap(...)` | Wraps the variable as a std map. | Can only be used in the top-level scope of the variable's first assignment. |
7777
| `toStdOrderedMap(...)` | Wraps the variable as a std ordered map. | Can only be used in the top-level scope of the variable's first assignment. |
78+
| `toStdList($valueType)` | Converts to an integer-key typed PHP array. | An identical contract copies directly; other sources receive strict key and value checks. |
79+
| `toStdDict($keyType, $valueType)` | Converts to a typed PHP dictionary. | Keys must be `Type::Int` or `Type::Str`; other rules match `toStdList()`. |
7880

7981
## Mechanisms not counted in this list
8082

docs/en/TYPED_ARRAYS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ PHP types may be omitted or declared as compatible storage types: `array` for
2424
`StdList` / `StdDict`, and `box` for `StdVector` / `StdMap` / `StdOrderedMap`.
2525
Explicit `mixed`, `any`, nullable types, unions, and incompatible types are rejected.
2626

27+
`$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.
28+
29+
**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.
30+
31+
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.
32+
2733
List keys are integers, including negative and sparse keys; no bounds checks
2834
are inserted. Only lists allow `[]` append. Dicts require an explicit int or
2935
string key. Dynamic `any` / `var` keys get internal strict type checks, not coercion.

docs/en/TYPE_ANNOTATIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Parameter entry validates the Box, container kind, leaf type, and full shape bef
9393

9494
## StdList / StdDict keys and values
9595

96-
Typed PHP arrays primarily establish their constraints statically. They introduce no runtime typed-array object and require no PHPX or HashTable changes.
96+
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.
9797

9898
- A list is an integer-key PHP array, allowing negative keys, sparse keys, and holes. It permits append.
9999
- A dict declares Int or Str keys and requires explicit keys, even for integer-key dicts.

docs/zh-cn/COMPILE_TIME_FUNCTIONS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,20 @@ TypePHP 不再为编译器指令保留任何全局函数名。编译期 API 最
6060
| `std::list($valueType)` | 创建整数键强类型 PHP 数组,支持负数、稀疏索引和追加。 | 新函数局部变量首次赋值;动态键严格检查,禁止动态引用修改。 |
6161
| `std::dict($keyType, $valueType)` | 创建强类型 PHP 字典,键为 `Type::Int``Type::Str`| 新函数局部变量首次赋值;必须显式提供键,不支持追加。 |
6262

63-
list/dict 保留普通 PHP 数组存储和写时复制,不需要 `toStd*()`。值要求静态类型匹配,`any` / `var` 键插入内部严格检查;仅允许只读动态 PHP 数组调用,禁止 `std::ref()`。参数使用完全一致的 `StdList` / `StdDict` 类型注解,PHP 类型可省略或为 `array`,不允许 `mixed`;同类型的原生 `&` 参数可以修改调用方。详细示例及边界见[强类型 PHP 数组与类型注解](TYPED_ARRAYS.md)
63+
list/dict 保留普通 PHP 数组存储和写时复制。值要求静态类型匹配,`any` / `var` 键插入内部严格检查;仅允许只读动态 PHP 数组调用,禁止 `std::ref()`。参数使用完全一致的 `StdList` / `StdDict` 类型注解,PHP 类型可省略或为 `array`,不允许 `mixed`;同类型的原生 `&` 参数可以修改调用方。详细示例及边界见[强类型 PHP 数组与类型注解](TYPED_ARRAYS.md)
6464

6565
## Std 容器转换关键词方法
6666

67-
当前 Std 容器转换关键词方法共 4 个。
67+
当前 Std 容器转换关键词方法共 6 个。
6868

6969
| 名称 | 作用 | 主要限制 |
7070
| --- | --- | --- |
7171
| `toStdArray(...)` | 将变量包装为 std array。 | 只能在变量首次赋值的顶层作用域使用。 |
7272
| `toStdVector(...)` | 将变量包装为 std vector。 | 只能在变量首次赋值的顶层作用域使用。 |
7373
| `toStdMap(...)` | 将变量包装为 std map。 | 只能在变量首次赋值的顶层作用域使用。 |
7474
| `toStdOrderedMap(...)` | 将变量包装为 std ordered map。 | 只能在变量首次赋值的顶层作用域使用。 |
75+
| `toStdList($valueType)` | 转为整数键强类型 PHP 数组。 | 同契约来源直接赋值;其他来源逐项严格校验键和值。 |
76+
| `toStdDict($keyType, $valueType)` | 转为强类型 PHP 字典。 | 键类型只能为 `Type::Int``Type::Str`;其他规则同 `toStdList()`|
7577

7678
## 不计入本文清单的机制
7779

docs/zh-cn/TYPED_ARRAYS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ function append(#[StdList(Type::Int)] array &$values): void
2121

2222
PHP 类型可以省略或声明为兼容类型:`StdList` / `StdDict` 对应 `array``StdVector` / `StdMap` / `StdOrderedMap` 对应 `box`。不允许显式 `mixed``any`、可空类型、联合类型和其他不兼容类型。
2323

24+
`$source->toStdList(Type::Int)``$source->toStdDict(Type::Str, Type::Int)` 可将值转换成新的局部强类型数组。同契约的强类型数组直接按 PHP 数组赋值;普通数组在运行时逐项严格校验键和值;其他值先经 `toArray()` 转为数组再校验。值类型也可写 `ClassName::class`,运行时要求每个值都是该类的实例。转换不会修改来源数组。
25+
26+
**性能提示:** 除同契约强类型数组的直接赋值外,转换会遍历整个数组,检查每个键和值,时间复杂度为 O(n)。非数组来源还要先执行 `toArray()`。大数组或循环中的反复转换可能明显增加耗时;应谨慎使用,尽量在数据进入强类型边界时转换一次,并复用结果。
27+
28+
校验依据数组在运行时实际保存的键类型。PHP 会把普通数组中的数字字符串键(如 `'123'`)规范化为整数键,因此这种数组不能通过 `toStdDict(Type::Str, ...)` 的严格校验;已是同契约 `StdDict` 的变量直接赋值,不会重新校验。
29+
2430
list 支持负数、稀疏整数键和空洞,不做边界检查;只有 list 允许 `[]` 追加。dict 必须显式提供 int 或 str 键。动态 `any` / `var` 键插入内部严格检查,不做隐式转换。值要求静态类型匹配,`Type::Any` 值除外。
2531

2632
局部强类型数组禁止通过 `std::ref()`、元素引用、可修改或引用传递的数组函数逃逸到动态 PHP。类型一致的原生参数可以按引用传递。字符串键 dict 遍历时将 PHP 数字键恢复为字符串,不修改 phpx 或底层 HashTable。

docs/zh-cn/TYPE_ANNOTATIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ StdArray 参数入口校验 Box、容器种类、叶子类型和完整形状,
9393

9494
## StdList / StdDict 的键和值
9595

96-
强类型 PHP 数组的约束主要在静态阶段建立,不增加运行时强类型数组对象,也不修改 PHPX 或 PHP HashTable
96+
强类型 PHP 数组的约束主要在静态阶段建立,不增加运行时强类型数组对象,也不修改 PHP HashTable。显式 `toStdList()` / `toStdDict()` 转换使用 PHPX `toTypedArray()` 逐项校验来源数组
9797

9898
- list 是整数键 PHP 数组,允许负数、稀疏键和空洞,不是连续序列;支持 `[]` 追加。
9999
- dict 的键只能声明为 Int 或 Str,必须显式提供键,包括整数键 dict 也不允许追加。

src/CompilerBase.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ class CompilerBase implements PropertyAccessContext
178178
'toString' => Type::STR,
179179
'toBool' => Type::BOOL,
180180
'toArray' => Type::ARRAY,
181+
'toStdList' => Type::ARRAY,
182+
'toStdDict' => Type::ARRAY,
181183
'toStream' => Type::STREAM,
182184
'toBigInt' => Type::BIGINT,
183185
'toBigFloat' => Type::BIGFLOAT,
@@ -190,6 +192,8 @@ class CompilerBase implements PropertyAccessContext
190192
/** Keyword methods not listed here accept no arguments. */
191193
public const array KEYWORD_METHOD_WITH_ARGUMENTS = [
192194
'toObject' => true,
195+
'toStdList' => true,
196+
'toStdDict' => true,
193197
];
194198

195199
private const array STREAM_FUNCTIONS = [

src/Parser/MethodCallTrait.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ protected function parseMethodCall(Expr\MethodCall $expr): string
610610
if ($this->containsNullsafeChain($expr->var)) {
611611
return $this->parseNullsafeExpr($expr);
612612
}
613+
if ($this->isNamedMethod($expr->name)
614+
&& in_array($expr->name->toString(), ['toStdList', 'toStdDict'], true)) {
615+
return $this->parseTypedArrayConversionCall($expr);
616+
}
613617

614618
$class = '';
615619
$materializedNativeReceiver = false;
@@ -1200,7 +1204,12 @@ private function parseExactLateStaticCall(
12001204

12011205
$calledCe = $this->getCalledCeExpr();
12021206
$direct = 'php::Var(' . self::PREFIX . $nativeFunc . '(this_))';
1203-
$fallback = 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))';
1207+
// Keep the receiver and qualify the runtime class: a child private
1208+
// method must not resolve as the parent's lexical private method.
1209+
$fallback = ($this->methodDef->flags & Modifiers::STATIC)
1210+
? 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))'
1211+
: 'php::callScoped(this_, php::concat({typephp_get_called_class(' . $calledCe . ')'
1212+
. ', "::", ' . $methodPtr . '}), ' . $this->getCallableScopeExpr() . ')';
12041213
return '(EXPECTED(' . $calledCe . ' == ' . $this->getClassEntryPtr($class) . ')'
12051214
. ' ? ' . $direct . ' : ' . $fallback . ')';
12061215
}
@@ -1227,6 +1236,7 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
12271236
$cacheCallable = false;
12281237
$directStaticCall = false;
12291238
$scopedStaticCall = false;
1239+
$scopedInstanceCall = false;
12301240
$staticCallTarget = '';
12311241
$staticCallMethod = '';
12321242
$canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name);
@@ -1291,7 +1301,11 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
12911301
}
12921302
$fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $staticCallMethod . '})';
12931303
$placeHolder = $fn;
1294-
if ($staticCallTarget !== '') {
1304+
if ($class === 'static' && $this->methodDef !== null
1305+
&& !($this->methodDef->flags & Modifiers::STATIC)) {
1306+
$scopedInstanceCall = true;
1307+
$fn = 'php::concat({' . $this->getCalledClassExpr() . ', "::", ' . $staticCallMethod . '})';
1308+
} elseif ($staticCallTarget !== '') {
12951309
$directStaticCall = true;
12961310
} else {
12971311
// `self::$method()` carries a lexical lookup class and a
@@ -1325,6 +1339,10 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
13251339
// Used to resolve the method signature when detecting by-reference arguments (late static binding is resolved within the current class hierarchy)
13261340
$rtFunc = $method;
13271341
$rtClass = $this->getFullClassName();
1342+
if ($this->methodDef !== null && !($this->methodDef->flags & Modifiers::STATIC)) {
1343+
$scopedInstanceCall = true;
1344+
$fn = 'php::concat({' . $this->getCalledClassExpr() . ', "::", ' . $methodPtr . '})';
1345+
}
13281346
} else {
13291347
if ($class === 'self') {
13301348
$class = $this->getFullClassName();
@@ -1391,6 +1409,9 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
13911409
}
13921410

13931411
if (empty($expr->args)) {
1412+
if ($scopedInstanceCall) {
1413+
return 'php::callScoped(this_, ' . $fn . ', ' . $this->getCallableScopeExpr() . ')';
1414+
}
13941415
if ($scopedStaticCall) {
13951416
return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ')';
13961417
}
@@ -1403,6 +1424,10 @@ protected function parseStaticCall(Expr\StaticCall $expr): string
14031424
return 'php::call(' . $fn . ')';
14041425
}
14051426
try {
1427+
if ($scopedInstanceCall) {
1428+
return 'php::callScoped(this_, ' . $fn . ', ' . $this->getCallableScopeExpr() . ', '
1429+
. $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')';
1430+
}
14061431
if ($scopedStaticCall) {
14071432
return 'php::callScoped(' . $fn . ', ' . $this->getCallableScopeExpr() . ', '
14081433
. $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')';

src/Parser/TypedArrayTrait.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ protected function getTypedArrayDefinition(NodeAbstract $expr): ?array
126126
if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) {
127127
return $this->getTypedArrayDefinition($expr->expr);
128128
}
129+
if ($expr instanceof Expr\MethodCall && $expr->name instanceof Node\Identifier) {
130+
$kind = match ($expr->name->name) {
131+
'toStdList' => 'list',
132+
'toStdDict' => 'dict',
133+
default => null,
134+
};
135+
if ($kind !== null) {
136+
return $this->parseTypedArrayDefinition($kind, $expr->args, $expr);
137+
}
138+
}
129139
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
130140
&& $expr->name instanceof Node\Identifier && $this->isStdClassExpr($expr->class)
131141
&& in_array(strtolower($expr->name->name), ['list', 'dict'], true)) {
@@ -137,6 +147,50 @@ protected function getTypedArrayDefinition(NodeAbstract $expr): ?array
137147
return null;
138148
}
139149

150+
protected function parseTypedArrayConversionCall(Expr\MethodCall $call): string
151+
{
152+
$kind = $call->name->toString() === 'toStdList' ? 'list' : 'dict';
153+
$definition = $this->parseTypedArrayDefinition($kind, $call->args, $call);
154+
if ($this->isVarExpr($call->var)) {
155+
$this->assertStdContainerDoesNotEscapeNativeObjects($call, $this->parseIdentifier($call->var));
156+
}
157+
// A matching typed array already satisfies the contract. Preserve
158+
// ordinary PHP array assignment and its copy-on-write behavior.
159+
if ($this->getTypedArrayDefinition($call->var) === $definition) {
160+
return $this->parseExprAsValue($call->var);
161+
}
162+
163+
$stdContainer = $this->isVarExpr($call->var)
164+
&& $this->isStdContainer($this->parseIdentifier($call->var));
165+
if ($this->detectTypeOfExpr($call->var) === Type::ARRAY && !$stdContainer) {
166+
$source = $this->parseExprAsValue($call->var);
167+
} else {
168+
$class = $this->detectClassOfExpr($call->var);
169+
if ($this->isNativeObjectClass($class)) {
170+
// Native objects use their declared toArray() method; their
171+
// pointer cannot be passed to PHPX's dynamic conversion.
172+
$source = $this->parseExprAsValue(new Expr\MethodCall($call->var, new Node\Identifier('toArray')));
173+
} else {
174+
$source = 'php::toArray(' . $this->parseExprAsValue($call->var) . ')';
175+
}
176+
}
177+
$valueType = match ($definition['type']) {
178+
Type::INT => 'Int',
179+
Type::FLOAT => 'Float',
180+
Type::BOOL => 'Bool',
181+
Type::STR => 'String',
182+
Type::ARRAY => 'Array',
183+
Type::OBJECT => 'Object',
184+
Type::VAR => 'Any',
185+
};
186+
$valueClass = $definition['class'] !== null && $definition['class'] !== ''
187+
? $this->getClassEntryPtr($definition['class'])
188+
: 'nullptr';
189+
return 'php::toTypedArray(' . $source . ', '
190+
. ($definition['keyType'] === Type::STR ? 'true' : 'false') . ', '
191+
. 'php::TypedArrayValueType::' . $valueType . ', ' . $valueClass . ')';
192+
}
193+
140194
protected function getTypedArrayAccessDefinition(NodeAbstract $expr): ?array
141195
{
142196
return $expr instanceof Expr\ArrayDimFetch ? $this->getTypedArrayDefinition($expr->var) : null;

0 commit comments

Comments
 (0)