feat(std): add array value initializer support for std containers

- Support single array parameter initialization for std::array, std::vector, std::map,
  std::orderedMap, std::list, and std::dict
- Implement type inference from array initializer expressions with exact type matching
- Add rectangular shape validation for std::array initializers
- Generate appropriate C++ code for value initializers using lambda expressions
- Update documentation to reflect new value initializer capabilities
- Add comprehensive tests for valid and invalid initializer scenarios
- Defer StdFunc/StdArgInfo implementation as documented in design records
master
韩天峰 3 weeks ago
parent 9d68d2803c
commit 5df39f2b4f
  1. 2
      docs/en/STD_FUNC_DESIGN.md
  2. 2
      docs/en/TYPED_ARRAYS.md
  3. 4
      docs/en/TYPE_ANNOTATIONS.md
  4. 2
      docs/zh-cn/STD_FUNC_DESIGN.md
  5. 2
      docs/zh-cn/TYPED_ARRAYS.md
  6. 4
      docs/zh-cn/TYPE_ANNOTATIONS.md
  7. 74
      phpunit/src/StdContainerValueInitializerTest.php
  8. 303
      src/Parser/StdContainerTrait.php
  9. 43
      src/Parser/TypedArrayTrait.php
  10. 12
      src/polyfills.php
  11. 88
      tests/compiler/std-array/value-initializers.phpt

@ -1,6 +1,6 @@
# StdFunc / StdArgInfo Type Annotation Design # StdFunc / StdArgInfo Type Annotation Design
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. Status: the `StdFunc` / `StdArgInfo` design is deferred and is not part of the current implementation plan; this document is retained only as a historical design record. The referenced StdArray annotation is implemented. See [unified type annotations](TYPE_ANNOTATIONS.md) for terminology and container contracts.
## Goals and non-goals ## Goals and non-goals

@ -5,6 +5,8 @@ This document records current implementation boundaries. See [Unified Type Annot
`std::list(T)` and `std::dict(K, V)` retain PHP array storage and copy-on-write. `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: Type annotations declare their key and value contracts on parameters and properties:
Local values may also infer their types from non-empty initializers, for example `std::list([9, 3, 5])` and `std::dict(['v1' => 999, 'v2' => 1000])`. Entries may contain arbitrary expressions, but every static value type must match exactly, and explicit dict keys must all have the same Int or Str type. Any var/any/mixed key or value is a compile error because it cannot provide a trustworthy inferred contract.
```php ```php
class State class State
{ {

@ -1,6 +1,6 @@
# Unified Type Annotation Design # Unified Type Annotation Design
Status: a combined record of existing container behavior and target design. The `StdArray` annotation is implemented; `StdFunc` and `StdArgInfo` are not. Status: a combined record of existing container behavior and target design. The `StdArray` annotation is implemented; `StdFunc` and `StdArgInfo` are deferred.
## Terminology and responsibilities ## Terminology and responsibilities
@ -27,6 +27,8 @@ See [StdFunc and StdArgInfo](STD_FUNC_DESIGN.md), [typed PHP arrays](TYPED_ARRAY
Both the `std::array` container and its `StdArray` annotation are implemented. The annotation declares parameter/property contracts; it does not create values. Both the `std::array` container and its `StdArray` annotation are implemented. The annotation declares parameter/property contracts; it does not create values.
Every value factory also accepts one non-empty PHP array initializer: `std::array($values)`, `std::vector($values)`, `std::map($values)`, `std::orderedMap($values)`, `std::list($values)`, and `std::dict($values)`. The compiler infers the contract from arbitrary key/value expressions whose static types are known. Types must match exactly; keys must be definite Int or Str values, and a var/any/mixed key or value is rejected. StdArray additionally infers and validates a rectangular shape. Empty arrays must use an explicit-type factory.
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. 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.
## Declarations and compatible PHP types ## Declarations and compatible PHP types

@ -1,6 +1,6 @@
# StdFunc / StdArgInfo 类型注解设计 # StdFunc / StdArgInfo 类型注解设计
状态:`StdFunc` / `StdArgInfo` 方案尚未实现;其中引用的 StdArray 类型注解已经实现。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。 状态:`StdFunc` / `StdArgInfo` 方案已暂缓,不进入当前实现计划;本文仅保留历史设计记录。其中引用的 StdArray 类型注解已经实现。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。
## 目标与非目标 ## 目标与非目标

@ -4,6 +4,8 @@
`std::list(T)` / `std::dict(K, V)` 保留普通 PHP 数组存储与写时复制,使用类型注解声明参数或属性的键和值类型: `std::list(T)` / `std::dict(K, V)` 保留普通 PHP 数组存储与写时复制,使用类型注解声明参数或属性的键和值类型:
局部值也可由非空数组直接初始化并推导类型,例如 `std::list([9, 3, 5])` 和 `std::dict(['v1' => 999, 'v2' => 1000])`。表达式不限于字面量,但所有静态 value 类型必须完全一致;dict 的显式 key 也必须全部为同一种 Int 或 Str。任何 var/any/mixed key/value 都因无法可靠推导而编译报错。
```php ```php
class State class State
{ {

@ -1,6 +1,6 @@
# 类型注解统一设计 # 类型注解统一设计
状态:现有容器实现与目标设计的统一记录;`StdArray` 类型注解已经实现,`StdFunc` / `StdArgInfo` 尚未实现。 状态:现有容器实现与目标设计的统一记录;`StdArray` 类型注解已经实现,`StdFunc` / `StdArgInfo` 已暂缓。
## 术语与职责 ## 术语与职责
@ -27,6 +27,8 @@
`std::array` 和 `StdArray` 类型注解均已实现。后者声明参数或属性契约,不创建容器值。 `std::array` 和 `StdArray` 类型注解均已实现。后者声明参数或属性契约,不创建容器值。
所有创建值的工厂还支持单个非空 PHP array 初始化参数:`std::array($values)`、`std::vector($values)`、`std::map($values)`、`std::orderedMap($values)`、`std::list($values)`、`std::dict($values)`。编译器从任意可静态定型的 key/value 表达式推导契约;类型必须完全一致,key 只能是确定的 Int 或 Str,key/value 为 var/any/mixed 时拒绝。StdArray 另外推导并验证矩形维度;空数组必须改用显式类型工厂。
不同存储模型不能因为类型参数相似就互换。Box 不等于 PHP array;类型注解也不改变 PHP 原生类型系统。 不同存储模型不能因为类型参数相似就互换。Box 不等于 PHP array;类型注解也不改变 PHP 原生类型系统。
## 声明与 PHP 类型兼容 ## 声明与 PHP 类型兼容

@ -0,0 +1,74 @@
<?php
use PHPUnit\Framework\Attributes\DataProvider;
use TypePhp\CompilerTest;
use TypePhp\Exception\SyntaxError;
use TypePhp\Exception\TestError;
final class StdContainerValueInitializerTest extends BaseTest
{
private function translate(string $source): string
{
global $translator;
$directory = sys_get_temp_dir() . '/std-value-initializer-' . bin2hex(random_bytes(6));
mkdir($directory);
$file = $directory . '/initializer.php';
file_put_contents($file, '<?php function main(): void { ' . $source . ' }');
try {
$translator = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator->addFiles([$file]);
$translator->prepareFile($file);
return file_get_contents($translator->convertFile($file));
} finally {
unlink($file);
rmdir($directory);
}
}
public function testEveryContainerAcceptsAnInferredArrayInitializer(): void
{
$code = $this->translate(<<<'PHP'
$n = 2;
$a = std::array([[1, $n], [3, 4]]);
$v = std::vector([1, $n]);
$m = std::map(['a' => 1, 'b' => $n]);
$o = std::orderedMap([1 => 'a', 2 => 'b']);
$l = std::list([1, $n]);
$d = std::dict(['a' => 1, 'b' => $n]);
PHP);
self::assertStringContainsString('php::StdArray<php::StdArray<php::Int, 2>, 2>', $code);
self::assertStringContainsString('a_ref.offsetGet(0).offsetSet(1, php::toInt(n))', $code);
self::assertStringContainsString('v_ref.push_back(php::toInt(n))', $code);
self::assertStringContainsString('m_ref.offsetSet(', $code);
self::assertStringContainsString('o_ref.offsetSet(', $code);
self::assertStringContainsString('l = php::Array{', $code);
self::assertStringContainsString('d = php::Array{', $code);
}
#[DataProvider('invalidInitializers')]
public function testInvalidInitializers(string $source, string $message): void
{
try {
$this->translate($source);
self::fail('Invalid std value initializer was accepted');
} catch (TestError|SyntaxError $error) {
self::assertStringContainsString($message, $error->getMessage());
}
}
public static function invalidInitializers(): iterable
{
yield 'empty' => ['$v = std::vector([]);', 'cannot infer types from an empty array'];
yield 'mixed values' => ['$v = std::vector([1, "2"]);', 'values must all have exactly the same type'];
yield 'mixed keys' => ['$m = std::map([1 => 1, "2" => 2]);', 'keys must all have exactly the same type'];
yield 'float key' => ['$m = std::map([1.5 => 1]);', 'statically known int or string type'];
yield 'bool key' => ['$m = std::map([true => 1]);', 'statically known int or string type'];
yield 'null key' => ['$m = std::map([null => 1]);', 'statically known int or string type'];
yield 'map missing key' => ['$m = std::map([1, 2]);', 'requires an explicit key'];
yield 'ragged array' => ['$a = std::array([[1], [2, 3]]);', 'uniform rectangular shape'];
yield 'list string key' => ['$l = std::list(["a" => 1]);', 'keys must have type int'];
yield 'dict missing key' => ['$d = std::dict([1, 2]);', 'requires an explicit key'];
yield 'dynamic key' => ['$key = std::any("a"); $d = std::dict([$key => 1]);', 'statically known int or string type'];
yield 'dynamic value' => ['$x = std::any(1); $l = std::list([$x]);', 'cannot infer an element type from var/any'];
}
}

@ -224,6 +224,19 @@ trait StdContainerTrait
return ''; return '';
} }
$initializer = $this->getStdValueInitializer($expr);
if ($initializer !== null) {
$inferred = $method === 'array'
? $this->inferStdArrayInitializer($initializer)
: $this->inferStdFlatInitializer(
$initializer,
$method === 'orderedmap' ? 'std::orderedMap' : 'std::' . $method,
$method === 'vector' ? 'positional' : 'map',
);
$class = $inferred['class'] ?? '';
return is_string($class) && $this->isNativeObjectClass($class) ? $class : '';
}
if ($method === 'array') { if ($method === 'array') {
$factory = $expr; $factory = $expr;
while ($factory instanceof StaticCall while ($factory instanceof StaticCall
@ -809,6 +822,175 @@ trait StdContainerTrait
}; };
} }
/** Return the one-array value-initializer overload, if this call uses it. */
protected function getStdValueInitializer(Expr\StaticCall $expr): ?Expr\Array_
{
if (count($expr->args) !== 1 || !$expr->args[0]->value instanceof Expr\Array_) {
return null;
}
$argument = $expr->args[0];
if ($argument->name !== null || $argument->unpack || $argument->byRef) {
$this->fatalError($argument, 'Std container value initialization requires one positional array argument');
}
return $argument->value;
}
/** @return array{type: string, class: ?string} */
protected function inferStdInitializerValueType(NodeAbstract $expr, string $owner): array
{
$this->assertExprCanBeUsedAsValue($expr, $owner . ' value');
$type = Type::getReferencedType($this->detectTypeOfExpr($expr));
if ($type === Type::VAR) {
$this->fatalError($expr, $owner . ' cannot infer an element type from var/any');
}
if (!in_array($type, [
Type::INT,
Type::FLOAT,
Type::BOOL,
Type::STR,
Type::ARRAY,
Type::OBJECT,
Type::BIGINT,
Type::BIGFLOAT,
Type::DECIMAL,
Type::STREAM,
Type::BOX,
], true)) {
$this->fatalError($expr, $owner . ' cannot infer a supported element type from this expression');
}
if ($type === Type::INT && $this->varIntTypes && $this->exprCanOverflowInt($expr)) {
$this->fatalError($expr, $owner . ' integer values that may widen require an explicit toInt() or native integer conversion');
}
$class = $type === Type::OBJECT ? $this->detectClassOfExpr($expr) : '';
return ['type' => $type, 'class' => $class !== '' ? $class : null];
}
protected function assertSameStdInitializerValueType(
array $expected,
array $actual,
NodeAbstract $expr,
string $owner,
): void {
if ($expected['type'] !== $actual['type']
|| strcasecmp($expected['class'] ?? '', $actual['class'] ?? '') !== 0
) {
$this->fatalError($expr, $owner . ' initializer values must all have exactly the same type');
}
}
/**
* Infer a one-dimensional initializer.
*
* @param 'positional'|'integer'|'map' $keyMode
* @return array{type: string, class: ?string, keyType?: string, items: array}
*/
protected function inferStdFlatInitializer(Expr\Array_ $array, string $owner, string $keyMode): array
{
if ($array->items === []) {
$this->fatalError($array, $owner . ' cannot infer types from an empty array');
}
$valueInfo = null;
$keyType = null;
foreach ($array->items as $item) {
if ($item === null || $item->unpack || $item->byRef) {
$this->fatalError($item ?? $array, $owner . ' initializer does not support holes, unpacking, or references');
}
if ($keyMode === 'positional' && $item->key !== null) {
$this->fatalError($item->key, $owner . ' initializer requires positional array elements');
}
if ($keyMode === 'map' && $item->key === null) {
$this->fatalError($item, $owner . ' initializer requires an explicit key for every value');
}
if ($item->key !== null && $keyMode !== 'positional') {
$actualKeyType = Type::getReferencedType($this->detectTypeOfExpr($item->key));
if (!in_array($actualKeyType, [Type::INT, Type::STR], true)) {
$this->fatalError($item->key, $owner . ' initializer keys must have a statically known int or string type');
}
if ($actualKeyType === Type::INT && $this->varIntTypes && $this->exprCanOverflowInt($item->key)) {
$this->fatalError($item->key, $owner . ' integer keys that may widen require an explicit toInt() or native integer conversion');
}
if ($keyMode === 'integer' && $actualKeyType !== Type::INT) {
$this->fatalError($item->key, $owner . ' initializer keys must have type int');
}
if ($keyType !== null && $keyType !== $actualKeyType) {
$this->fatalError($item->key, $owner . ' initializer keys must all have exactly the same type');
}
$keyType = $actualKeyType;
} elseif ($keyMode === 'integer') {
$keyType ??= Type::INT;
}
$actualValueInfo = $this->inferStdInitializerValueType($item->value, $owner);
if ($valueInfo === null) {
$valueInfo = $actualValueInfo;
} else {
$this->assertSameStdInitializerValueType($valueInfo, $actualValueInfo, $item->value, $owner);
}
}
$result = $valueInfo + ['items' => $array->items];
if ($keyMode !== 'positional') {
$result['keyType'] = $keyType ?? Type::INT;
}
return $result;
}
/**
* Infer the leaf type and rectangular shape of a nested std::array value.
*
* @return array{type: string, class: ?string, dimensions: list<int>, entries: list<array{path: list<int>, value: NodeAbstract}>}
*/
protected function inferStdArrayInitializer(Expr\Array_ $array, string $owner = 'std::array'): array
{
if ($array->items === []) {
$this->fatalError($array, $owner . ' cannot infer types or dimensions from an empty array');
}
$leafInfo = null;
$childDimensions = null;
$entries = [];
$nested = null;
foreach ($array->items as $index => $item) {
if ($item === null || $item->unpack || $item->byRef || $item->key !== null) {
$this->fatalError($item ?? $array, $owner . ' initializer must be a positional array without holes, unpacking, or references');
}
$isNested = $item->value instanceof Expr\Array_;
if ($nested !== null && $nested !== $isNested) {
$this->fatalError($item->value, $owner . ' initializer must have a uniform rectangular shape');
}
$nested = $isNested;
if ($isNested) {
$child = $this->inferStdArrayInitializer($item->value, $owner);
if ($childDimensions !== null && $childDimensions !== $child['dimensions']) {
$this->fatalError($item->value, $owner . ' initializer must have a uniform rectangular shape');
}
$childDimensions = $child['dimensions'];
$actualLeafInfo = ['type' => $child['type'], 'class' => $child['class']];
foreach ($child['entries'] as $entry) {
array_unshift($entry['path'], $index);
$entries[] = $entry;
}
} else {
$actualLeafInfo = $this->inferStdInitializerValueType($item->value, $owner);
$entries[] = ['path' => [$index], 'value' => $item->value];
}
if ($leafInfo === null) {
$leafInfo = $actualLeafInfo;
} else {
$this->assertSameStdInitializerValueType($leafInfo, $actualLeafInfo, $item->value, $owner);
}
}
return [
'type' => $leafInfo['type'],
'class' => $leafInfo['class'],
'dimensions' => [count($array->items), ...($childDimensions ?? [])],
'entries' => $entries,
];
}
protected function parseStdContainerOffsetUnset(Expr\ArrayDimFetch $expr): string protected function parseStdContainerOffsetUnset(Expr\ArrayDimFetch $expr): string
{ {
if ($expr->dim === null) { if ($expr->dim === null) {
@ -933,7 +1115,11 @@ trait StdContainerTrait
protected function convertStdValueExpr(array $info, NodeAbstract $expr): string protected function convertStdValueExpr(array $info, NodeAbstract $expr): string
{ {
$valueExpr = $this->parseExpr($expr); return $this->convertParsedStdValueExpr($info, $expr, $this->parseExpr($expr));
}
protected function convertParsedStdValueExpr(array $info, NodeAbstract $expr, string $valueExpr): string
{
$class = $info['class'] ?? null; $class = $info['class'] ?? null;
if ($class === null) { if ($class === null) {
$targetType = $info['type']; $targetType = $info['type'];
@ -966,6 +1152,66 @@ trait StdContainerTrait
return 'php::toObject(' . $valueExpr . ', ' . $this->getClassEntryPtr($class) . ')'; return 'php::toObject(' . $valueExpr . ', ' . $this->getClassEntryPtr($class) . ')';
} }
protected function parseOrderedStdInitializerValue(array $info, NodeAbstract $expr): string
{
$value = $this->materializeRefReturnAsValue(
$expr,
$this->parseOrderedOperand($expr, false),
);
return $this->convertParsedStdValueExpr($info, $expr, $value);
}
/** @param list<string> $statements */
private function genStdValueInitializerExpression(string $var, array $statements): string
{
$code = '[&]() -> php::Var {' . PHP_EOL;
$this->indentLevel++;
foreach ($statements as $statement) {
$code .= $this->getIndent() . $statement . ';' . PHP_EOL;
}
$code .= $this->getIndent() . 'return ' . $var . ';' . PHP_EOL;
$this->indentLevel--;
return $code . $this->getIndent() . '}()';
}
private function genStdArrayValueInitializer(string $var, array $info, array $entries): string
{
$statements = [];
foreach ($entries as $entry) {
$path = $entry['path'];
$last = array_pop($path);
$target = $var . '_ref';
foreach ($path as $index) {
$target .= '.offsetGet(' . $index . ')';
}
$value = $this->parseOrderedStdInitializerValue($info, $entry['value']);
$statements[] = $target . '.offsetSet(' . $last . ', ' . $value . ')';
}
return $this->genStdValueInitializerExpression($var, $statements);
}
private function genStdVectorValueInitializer(string $var, array $info, array $items): string
{
$statements = [];
foreach ($items as $item) {
$statements[] = $var . '_ref.push_back('
. $this->parseOrderedStdInitializerValue($info, $item->value) . ')';
}
return $this->genStdValueInitializerExpression($var, $statements);
}
private function genStdMapValueInitializer(string $var, array $info, array $items): string
{
$statements = [];
foreach ($items as $item) {
$key = $this->parseOrderedOperand($item->key, false);
$key = $this->convertStdContainerKey($info, $key);
$value = $this->parseOrderedStdInitializerValue($info, $item->value);
$statements[] = $var . '_ref.offsetSet(' . $key . ', ' . $value . ')';
}
return $this->genStdValueInitializerExpression($var, $statements);
}
protected function convertStdVarBackedExpr(string $targetType, string $valueExpr, NodeAbstract $expr): string protected function convertStdVarBackedExpr(string $targetType, string $valueExpr, NodeAbstract $expr): string
{ {
$sourceType = $this->detectTypeOfExpr($expr); $sourceType = $this->detectTypeOfExpr($expr);
@ -1060,6 +1306,25 @@ trait StdContainerTrait
protected function parseStdArray(string $var, Expr\StaticCall $expr): string protected function parseStdArray(string $var, Expr\StaticCall $expr): string
{ {
$initializer = $this->getStdValueInitializer($expr);
if ($initializer !== null) {
$inferred = $this->inferStdArrayInitializer($initializer);
$type = $inferred['type'];
$dimensions = $inferred['dimensions'];
$totalElements = array_product($dimensions);
$info = [
'kind' => 'array',
'decl' => $this->getStdArrayDecl($type, $dimensions, $inferred['class']),
'type' => $type,
'class' => $inferred['class'],
'sizes' => array_reverse($dimensions),
'dimensions' => $dimensions,
'bytes' => $totalElements * $this->getStdValueTypeBytes($type),
];
$this->context->stdArrays[$var] = $this->addStdTypeId($info);
return $this->genStdArrayValueInitializer($var, $info, $inferred['entries']);
}
$tmp = $expr; $tmp = $expr;
$nesting = []; $nesting = [];
$totalBytes = 0; $totalBytes = 0;
@ -1107,6 +1372,22 @@ trait StdContainerTrait
protected function parseStdVector(string $var, Expr\StaticCall $expr): string protected function parseStdVector(string $var, Expr\StaticCall $expr): string
{ {
$initializer = $this->getStdValueInitializer($expr);
if ($initializer !== null) {
$inferred = $this->inferStdFlatInitializer($initializer, 'std::vector', 'positional');
$decl = Type::STD_VECTOR . '<'
. $this->getStdContainerElementType($inferred['type'], $inferred['class']) . '>';
$info = [
'kind' => 'vector',
'decl' => $decl,
'type' => $inferred['type'],
'class' => $inferred['class'],
'size' => null,
];
$this->context->stdContainers[$var] = $this->addStdTypeId($info);
return $this->genStdVectorValueInitializer($var, $info, $inferred['items']);
}
if (count($expr->args) < 1 || count($expr->args) > 2) { if (count($expr->args) < 1 || count($expr->args) > 2) {
$this->fatalError($expr, 'std::vector() expects one or two arguments'); $this->fatalError($expr, 'std::vector() expects one or two arguments');
} }
@ -1143,6 +1424,26 @@ trait StdContainerTrait
private function parseStdMapBase(string $var, Expr\StaticCall $expr, string $funcName, string $containerType, string $kind): string private function parseStdMapBase(string $var, Expr\StaticCall $expr, string $funcName, string $containerType, string $kind): string
{ {
$initializer = $this->getStdValueInitializer($expr);
if ($initializer !== null) {
$inferred = $this->inferStdFlatInitializer($initializer, $funcName, 'map');
$decl = $this->getStdMapDecl(
$containerType,
$inferred['keyType'],
$inferred['type'],
$inferred['class'],
);
$info = [
'kind' => $kind,
'decl' => $decl,
'type' => $inferred['type'],
'class' => $inferred['class'],
'keyType' => $inferred['keyType'],
];
$this->context->stdContainers[$var] = $this->addStdTypeId($info);
return $this->genStdMapValueInitializer($var, $info, $inferred['items']);
}
if (count($expr->args) !== 2) { if (count($expr->args) !== 2) {
$this->fatalError($expr, $funcName . '() expects two arguments'); $this->fatalError($expr, $funcName . '() expects two arguments');
} }

@ -69,6 +69,39 @@ trait TypedArrayTrait
return ['kind' => $kind, 'keyType' => $key, 'type' => $value['type'], 'class' => $value['class']]; return ['kind' => $kind, 'keyType' => $key, 'type' => $value['type'], 'class' => $value['class']];
} }
protected function parseTypedArrayFactoryDefinition(string $kind, Expr\StaticCall $call): array
{
$initializer = $this->getStdValueInitializer($call);
if ($initializer === null) {
return $this->parseTypedArrayDefinition($kind, $call->args, $call);
}
$inferred = $this->inferStdFlatInitializer(
$initializer,
'std::' . $kind,
$kind === 'list' ? 'integer' : 'map',
);
if (!in_array($inferred['type'], [
Type::INT,
Type::FLOAT,
Type::BOOL,
Type::STR,
Type::ARRAY,
Type::OBJECT,
], true)) {
$this->fatalError($call, 'Typed PHP array value initialization only supports concrete PHP value types');
}
if ($this->isNativeObjectClass($inferred['class'] ?? '')) {
$this->fatalError($call, 'Typed PHP arrays cannot hold Native objects');
}
return [
'kind' => $kind,
'keyType' => $kind === 'list' ? Type::INT : $inferred['keyType'],
'type' => $inferred['type'],
'class' => $inferred['class'],
];
}
protected function parseTypedArrayParameterDefinition(Node\Param $param): ?array protected function parseTypedArrayParameterDefinition(Node\Param $param): ?array
{ {
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) { foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) {
@ -96,7 +129,7 @@ trait TypedArrayTrait
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
&& $expr->name instanceof Node\Identifier && $this->isStdClassExpr($expr->class) && $expr->name instanceof Node\Identifier && $this->isStdClassExpr($expr->class)
&& in_array(strtolower($expr->name->name), ['list', 'dict'], true)) { && in_array(strtolower($expr->name->name), ['list', 'dict'], true)) {
return $this->parseTypedArrayDefinition(strtolower($expr->name->name), $expr->args, $expr); return $this->parseTypedArrayFactoryDefinition(strtolower($expr->name->name), $expr);
} }
if ($this->isVarExpr($expr)) { if ($this->isVarExpr($expr)) {
return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null; return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null;
@ -137,7 +170,7 @@ trait TypedArrayTrait
&& $right->name instanceof Node\Identifier && $this->isStdClassExpr($right->class) && $right->name instanceof Node\Identifier && $this->isStdClassExpr($right->class)
&& in_array(strtolower($right->name->name), ['list', 'dict'], true); && in_array(strtolower($right->name->name), ['list', 'dict'], true);
$definition = $factory $definition = $factory
? $this->parseTypedArrayDefinition(strtolower($right->name->name), $right->args, $right) ? $this->parseTypedArrayFactoryDefinition(strtolower($right->name->name), $right)
: $this->getTypedArrayDefinition($right); : $this->getTypedArrayDefinition($right);
$existing = $this->context->typedArrays[$name] ?? null; $existing = $this->context->typedArrays[$name] ?? null;
if ($definition === null && $existing === null) { if ($definition === null && $existing === null) {
@ -165,7 +198,11 @@ trait TypedArrayTrait
$this->addLocalVar($name, Type::ARRAY); $this->addLocalVar($name, Type::ARRAY);
$this->context->typedArrays[$name] = $definition; $this->context->typedArrays[$name] = $definition;
} }
return $name . ' = ' . ($factory ? 'php::Array{}' : $this->parseExprAsValue($right)); if ($factory) {
$initializer = $this->getStdValueInitializer($right);
return $name . ' = ' . ($initializer === null ? 'php::Array{}' : $this->parseArray($initializer));
}
return $name . ' = ' . $this->parseExprAsValue($right);
} }
protected function guardTypedArrayValue(array $def, Expr $expr, string $code, bool $key = false): string protected function guardTypedArrayValue(array $def, Expr $expr, string $code, bool $key = false): string

@ -247,32 +247,32 @@ class std
return (bool) $condition; return (bool) $condition;
} }
public static function array(mixed $type, int $size): array public static function array(mixed $typeOrValues, ?int $size = null): array
{ {
return []; return [];
} }
public static function orderedMap(mixed $key_type, mixed $value_type): array public static function orderedMap(mixed $keyTypeOrValues, mixed $valueType = null): array
{ {
return []; return [];
} }
public static function map(mixed $key_type, mixed $value_type): array public static function map(mixed $keyTypeOrValues, mixed $valueType = null): array
{ {
return []; return [];
} }
public static function vector(mixed $value_type, ?int $size = null): array public static function vector(mixed $valueTypeOrValues, ?int $size = null): array
{ {
return []; return [];
} }
public static function list(mixed $valueType): array public static function list(mixed $valueTypeOrValues): array
{ {
return []; return [];
} }
public static function dict(mixed $keyType, mixed $valueType): array public static function dict(mixed $keyTypeOrValues, mixed $valueType = null): array
{ {
return []; return [];
} }

@ -0,0 +1,88 @@
--TEST--
All std containers infer their contracts from array value initializers
--FILE--
<?php
function typed_int(int $value): int
{
return $value;
}
function typed_string(string $value): string
{
return $value;
}
function next_value(int &$counter): int
{
return ++$counter;
}
function main(): void
{
$base = 2;
$array = std::array([
[1, $base + 1, typed_int(4)],
[0, typed_int(2), 4],
]);
var_dump(count($array), count($array[0]), $array[0][1], $array[1][1]);
$vector = std::vector([typed_int(7), $base + 6, 9]);
var_dump(count($vector), $vector[0], $vector[1], $vector[2]);
$counter = 0;
$evaluated = std::vector([next_value($counter), next_value($counter)]);
var_dump($evaluated[0], $evaluated[1], $counter);
$map = std::map([
typed_string('v1') => typed_int(999),
'v2' => $base + 998,
]);
var_dump($map['v1'], $map['v2']);
$intMap = std::map([
typed_int(10) => typed_string('ten'),
20 => 'twenty',
]);
var_dump($intMap[10], $intMap[20]);
$ordered = std::orderedMap([
20 => typed_string('twenty'),
typed_int(10) => 'ten',
]);
var_dump($ordered[10], $ordered[20]);
$list = std::list([typed_int(9), $base + 1, 5]);
var_dump(is_array($list), $list[0], $list[1], $list[2]);
$dict = std::dict([
typed_string('v1') => typed_int(999),
'v2' => $base + 998,
]);
var_dump(is_array($dict), $dict['v1'], $dict['v2']);
}
?>
--EXPECT--
int(2)
int(3)
int(3)
int(2)
int(3)
int(7)
int(8)
int(9)
int(1)
int(2)
int(2)
int(999)
int(1000)
string(3) "ten"
string(6) "twenty"
string(3) "ten"
string(6) "twenty"
bool(true)
int(9)
int(3)
int(5)
bool(true)
int(999)
int(1000)
Loading…
Cancel
Save