diff --git a/docs/en/STD_FUNC_DESIGN.md b/docs/en/STD_FUNC_DESIGN.md index d80b7676..82a62363 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: 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 diff --git a/docs/en/TYPED_ARRAYS.md b/docs/en/TYPED_ARRAYS.md index c73f1e10..fd7f50bc 100644 --- a/docs/en/TYPED_ARRAYS.md +++ b/docs/en/TYPED_ARRAYS.md @@ -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. 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 class State { diff --git a/docs/en/TYPE_ANNOTATIONS.md b/docs/en/TYPE_ANNOTATIONS.md index 886801ae..cf3f5374 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 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 @@ -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. +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. ## Declarations and compatible PHP types diff --git a/docs/zh-cn/STD_FUNC_DESIGN.md b/docs/zh-cn/STD_FUNC_DESIGN.md index e8436c5b..33a0bbb4 100644 --- a/docs/zh-cn/STD_FUNC_DESIGN.md +++ b/docs/zh-cn/STD_FUNC_DESIGN.md @@ -1,6 +1,6 @@ # StdFunc / StdArgInfo 类型注解设计 -状态:`StdFunc` / `StdArgInfo` 方案尚未实现;其中引用的 StdArray 类型注解已经实现。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。 +状态:`StdFunc` / `StdArgInfo` 方案已暂缓,不进入当前实现计划;本文仅保留历史设计记录。其中引用的 StdArray 类型注解已经实现。统一术语及容器类型见 [类型注解统一设计](TYPE_ANNOTATIONS.md)。 ## 目标与非目标 diff --git a/docs/zh-cn/TYPED_ARRAYS.md b/docs/zh-cn/TYPED_ARRAYS.md index e7ae0da2..2917ddcc 100644 --- a/docs/zh-cn/TYPED_ARRAYS.md +++ b/docs/zh-cn/TYPED_ARRAYS.md @@ -4,6 +4,8 @@ `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 class State { diff --git a/docs/zh-cn/TYPE_ANNOTATIONS.md b/docs/zh-cn/TYPE_ANNOTATIONS.md index 226fdafd..5179dd75 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` 已暂缓。 ## 术语与职责 @@ -27,6 +27,8 @@ `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 原生类型系统。 ## 声明与 PHP 类型兼容 diff --git a/phpunit/src/StdContainerValueInitializerTest.php b/phpunit/src/StdContainerValueInitializerTest.php new file mode 100644 index 00000000..7b55a61f --- /dev/null +++ b/phpunit/src/StdContainerValueInitializerTest.php @@ -0,0 +1,74 @@ +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, 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']; + } +} diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index 33fa2d2e..ebb5d8af 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -224,6 +224,19 @@ trait StdContainerTrait 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') { $factory = $expr; 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, entries: list, 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 { if ($expr->dim === null) { @@ -933,7 +1115,11 @@ trait StdContainerTrait 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; if ($class === null) { $targetType = $info['type']; @@ -966,6 +1152,66 @@ trait StdContainerTrait 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 $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 { $sourceType = $this->detectTypeOfExpr($expr); @@ -1060,6 +1306,25 @@ trait StdContainerTrait 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; $nesting = []; $totalBytes = 0; @@ -1107,6 +1372,22 @@ trait StdContainerTrait 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) { $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 { + $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) { $this->fatalError($expr, $funcName . '() expects two arguments'); } diff --git a/src/Parser/TypedArrayTrait.php b/src/Parser/TypedArrayTrait.php index dddafbcd..572e5c6c 100644 --- a/src/Parser/TypedArrayTrait.php +++ b/src/Parser/TypedArrayTrait.php @@ -69,6 +69,39 @@ trait TypedArrayTrait 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 { foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) { @@ -96,7 +129,7 @@ trait TypedArrayTrait 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)) { - return $this->parseTypedArrayDefinition(strtolower($expr->name->name), $expr->args, $expr); + return $this->parseTypedArrayFactoryDefinition(strtolower($expr->name->name), $expr); } if ($this->isVarExpr($expr)) { return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null; @@ -137,7 +170,7 @@ trait TypedArrayTrait && $right->name instanceof Node\Identifier && $this->isStdClassExpr($right->class) && in_array(strtolower($right->name->name), ['list', 'dict'], true); $definition = $factory - ? $this->parseTypedArrayDefinition(strtolower($right->name->name), $right->args, $right) + ? $this->parseTypedArrayFactoryDefinition(strtolower($right->name->name), $right) : $this->getTypedArrayDefinition($right); $existing = $this->context->typedArrays[$name] ?? null; if ($definition === null && $existing === null) { @@ -165,7 +198,11 @@ trait TypedArrayTrait $this->addLocalVar($name, Type::ARRAY); $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 diff --git a/src/polyfills.php b/src/polyfills.php index 3d9afc66..15b71c07 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -247,32 +247,32 @@ class std return (bool) $condition; } - public static function array(mixed $type, int $size): array + public static function array(mixed $typeOrValues, ?int $size = null): array { return []; } - public static function orderedMap(mixed $key_type, mixed $value_type): array + public static function orderedMap(mixed $keyTypeOrValues, mixed $valueType = null): array { return []; } - public static function map(mixed $key_type, mixed $value_type): array + public static function map(mixed $keyTypeOrValues, mixed $valueType = null): array { return []; } - public static function vector(mixed $value_type, ?int $size = null): array + public static function vector(mixed $valueTypeOrValues, ?int $size = null): array { return []; } - public static function list(mixed $valueType): array + public static function list(mixed $valueTypeOrValues): array { return []; } - public static function dict(mixed $keyType, mixed $valueType): array + public static function dict(mixed $keyTypeOrValues, mixed $valueType = null): array { return []; } diff --git a/tests/compiler/std-array/value-initializers.phpt b/tests/compiler/std-array/value-initializers.phpt new file mode 100644 index 00000000..cdce0163 --- /dev/null +++ b/tests/compiler/std-array/value-initializers.phpt @@ -0,0 +1,88 @@ +--TEST-- +All std containers infer their contracts from array value initializers +--FILE-- + 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)