- Add typedArray property to ArgInfo entity for strong array contracts - Remove deprecated ArrayDef functionality and related test files - Implement typed array access validation and assignment handling - Add typed array support in closure generation and variable binding - Introduce compilation error checks for typed array references - Add comprehensive typed array attribute and property tests - Update call argument generator to validate typed array arguments - Implement typed array compound write restrictions - Add typed array alias and rebind safety checksmaster
parent
8da7b5b4e5
commit
f3f686ec46
74 changed files with 1530 additions and 706 deletions
@ -1,42 +0,0 @@ |
||||
# `#[ArrayDef]` compile-time array contracts |
||||
|
||||
`#[ArrayDef]` attaches key/value type information to a property declared |
||||
exactly as `array`. It supports both Zend classes and `#[Native]` classes and |
||||
has no runtime metadata or per-read overhead. |
||||
|
||||
```php |
||||
class Index |
||||
{ |
||||
#[ArrayDef(Type::String)] |
||||
public array $names = []; // list<string> |
||||
|
||||
#[ArrayDef(Type::Int, Type::String)] |
||||
public array $labels = []; // map<int, string> |
||||
} |
||||
``` |
||||
|
||||
One argument defines a list value type. Two arguments define a map key type |
||||
and value type. Map keys are restricted to `Type::Int` or `Type::String`. |
||||
`ClassName::class` is therefore valid only as a list element type or as the |
||||
second (value) argument of a map. |
||||
|
||||
For direct writes whose expression types are known, the compiler either emits |
||||
the normal write unchanged or reports a fatal type error. An `any` key/value is |
||||
checked with PHPX exact-type helpers at runtime. No coercive `intval()` or |
||||
string conversion is performed. |
||||
|
||||
List writes support `[]` and non-negative integer indexes up to PHP's current |
||||
append position. Indexed writes uniformly emit `php::safeArrayIndex(index, |
||||
array)`. The helper uses `zend_hash_next_free_element()` and follows the |
||||
initial-index rule of `zend_hash_next_index_insert()`, which remains correct |
||||
when `unset()` has created holes or removed the highest numeric key. An index |
||||
equal to that value behaves like the next `$array[]` append; earlier indexes |
||||
may update or refill an element. Negative indexes and indexes beyond the append |
||||
position fail at runtime. There is no AST special case for |
||||
`property[count(property)]`. Maps do not support `[]` append writes. |
||||
|
||||
The contract intentionally applies only to direct element assignment lowered |
||||
by TypePHP. Reads and in-place operators are unchanged. Values passed through |
||||
dynamic functions, callbacks, Reflection, `eval()`, or other ZendVM escape |
||||
paths are outside the contract and have undefined behavior from ArrayDef's |
||||
perspective. |
||||
@ -0,0 +1,36 @@ |
||||
# Typed PHP arrays and type annotations |
||||
|
||||
`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: |
||||
|
||||
```php |
||||
class State |
||||
{ |
||||
#[StdList(Type::Int)] public array $values = []; |
||||
#[StdDict(Type::Str, Type::Int)] public $counts = []; |
||||
} |
||||
|
||||
function append(#[StdList(Type::Int)] array &$values): void |
||||
{ |
||||
$values[] = 42; |
||||
} |
||||
``` |
||||
|
||||
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. |
||||
|
||||
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. |
||||
Values require matching static types, except for `Type::Any` values. |
||||
|
||||
Local typed arrays cannot escape to dynamic PHP through `std::ref()`, element |
||||
references, or mutable/by-reference array functions. Matching annotated native |
||||
parameters may accept references. String-key dict iteration converts numeric |
||||
PHP keys back to strings without changing PHPX or HashTable storage. |
||||
|
||||
Property annotations currently check first-level direct element assignments. |
||||
They do not provide complete protection against whole-property replacement, |
||||
dynamic PHP object mutation, or object/property reference escapes. Property reads |
||||
are not automatically promoted to closed-contract local typed arrays. |
||||
@ -0,0 +1,24 @@ |
||||
# 强类型 PHP 数组与类型注解 |
||||
|
||||
`std::list(T)` / `std::dict(K, V)` 保留普通 PHP 数组存储与写时复制,使用类型注解声明参数或属性的键和值类型: |
||||
|
||||
```php |
||||
class State |
||||
{ |
||||
#[StdList(Type::Int)] public array $values = []; |
||||
#[StdDict(Type::Str, Type::Int)] public $counts = []; |
||||
} |
||||
|
||||
function append(#[StdList(Type::Int)] array &$values): void |
||||
{ |
||||
$values[] = 42; |
||||
} |
||||
``` |
||||
|
||||
PHP 类型可以省略或声明为兼容类型:`StdList` / `StdDict` 对应 `array`,`StdVector` / `StdMap` / `StdOrderedMap` 对应 `box`。不允许显式 `mixed`、`any`、可空类型、联合类型和其他不兼容类型。 |
||||
|
||||
list 支持负数、稀疏整数键和空洞,不做边界检查;只有 list 允许 `[]` 追加。dict 必须显式提供 int 或 str 键。动态 `any` / `var` 键插入内部严格检查,不做隐式转换。值要求静态类型匹配,`Type::Any` 值除外。 |
||||
|
||||
局部强类型数组禁止通过 `std::ref()`、元素引用、可修改或引用传递的数组函数逃逸到动态 PHP。类型一致的原生参数可以按引用传递。字符串键 dict 遍历时将 PHP 数字键恢复为字符串,不修改 phpx 或底层 HashTable。 |
||||
|
||||
当前属性类型注解检查第一层直接元素赋值,并不完整保护整属性替换、动态 PHP 对象修改或对象/属性引用逃逸。属性读取不会自动升级为受封闭契约保护的局部强类型数组。 |
||||
@ -1,7 +0,0 @@ |
||||
<?php |
||||
class ArrayDefClassMapKeyType {} |
||||
class ArrayDefClassMapKeyBox |
||||
{ |
||||
#[ArrayDef(ArrayDefClassMapKeyType::class, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
@ -1,13 +0,0 @@ |
||||
<?php |
||||
|
||||
class ArrayDefInclusiveUpperBound |
||||
{ |
||||
#[ArrayDef(Type::String)] |
||||
public array $values = []; |
||||
} |
||||
|
||||
function writeArrayDefInclusiveUpperBound(ArrayDefInclusiveUpperBound $box, int $index): void |
||||
{ |
||||
$box->values[$index] = 'indexed'; |
||||
$box->values[count($box->values)] = 'counted'; |
||||
} |
||||
@ -1,6 +0,0 @@ |
||||
<?php |
||||
class ArrayDefInvalidMapKey |
||||
{ |
||||
#[ArrayDef(Type::Bool, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
@ -1,10 +0,0 @@ |
||||
<?php |
||||
class ArrayDefMapAppend |
||||
{ |
||||
#[ArrayDef(Type::Int, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function arrayDefMapAppend(ArrayDefMapAppend $box): void |
||||
{ |
||||
$box->value[] = 'bad'; |
||||
} |
||||
@ -1,8 +0,0 @@ |
||||
<?php |
||||
#[Native] |
||||
class ArrayDefNativeValue {} |
||||
class ArrayDefNativeValueBox |
||||
{ |
||||
#[ArrayDef(ArrayDefNativeValue::class)] |
||||
public array $values = []; |
||||
} |
||||
@ -1,6 +0,0 @@ |
||||
<?php |
||||
class ArrayDefNoArguments |
||||
{ |
||||
#[ArrayDef] |
||||
public array $value = []; |
||||
} |
||||
@ -1,6 +0,0 @@ |
||||
<?php |
||||
class ArrayDefNonArrayProperty |
||||
{ |
||||
#[ArrayDef(Type::Int)] |
||||
public string $value = ''; |
||||
} |
||||
@ -1,12 +0,0 @@ |
||||
<?php |
||||
class ArrayDefExpectedUser {} |
||||
class ArrayDefOtherUser {} |
||||
class ArrayDefClassMismatchBox |
||||
{ |
||||
#[ArrayDef(ArrayDefExpectedUser::class)] |
||||
public array $users = []; |
||||
} |
||||
function arrayDefStaticClassMismatch(ArrayDefClassMismatchBox $box): void |
||||
{ |
||||
$box->users[] = new ArrayDefOtherUser(); |
||||
} |
||||
@ -1,10 +0,0 @@ |
||||
<?php |
||||
class ArrayDefStaticKeyMismatch |
||||
{ |
||||
#[ArrayDef(Type::Int, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function arrayDefStaticKeyMismatch(ArrayDefStaticKeyMismatch $box): void |
||||
{ |
||||
$box->value['bad'] = 'value'; |
||||
} |
||||
@ -1,11 +0,0 @@ |
||||
<?php |
||||
#[Native] |
||||
class ArrayDefStaticValueMismatch |
||||
{ |
||||
#[ArrayDef(Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function arrayDefStaticValueMismatch(ArrayDefStaticValueMismatch $box): void |
||||
{ |
||||
$box->value[] = 123; |
||||
} |
||||
@ -1,11 +0,0 @@ |
||||
<?php |
||||
class ArrayDefStdContainerValueBox |
||||
{ |
||||
#[ArrayDef(Type::Any)] |
||||
public array $values = []; |
||||
} |
||||
function arrayDefStdContainerValue(ArrayDefStdContainerValueBox $box): void |
||||
{ |
||||
$values = std::vector(Type::Int); |
||||
$box->values[] = $values; |
||||
} |
||||
@ -1,6 +0,0 @@ |
||||
<?php |
||||
class ArrayDefTooManyArguments |
||||
{ |
||||
#[ArrayDef(Type::Int, Type::String, Type::Bool)] |
||||
public array $value = []; |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
class TypedArrayPropertyClassMapKeyType {} |
||||
class TypedArrayPropertyClassMapKeyBox |
||||
{ |
||||
#[StdDict(TypedArrayPropertyClassMapKeyType::class, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
class TypedArrayPropertyInclusiveUpperBound |
||||
{ |
||||
#[StdList(Type::String)] |
||||
public array $values = []; |
||||
} |
||||
|
||||
function writeTypedArrayPropertyInclusiveUpperBound(TypedArrayPropertyInclusiveUpperBound $box, int $index): void |
||||
{ |
||||
$box->values[$index] = 'indexed'; |
||||
$box->values[count($box->values)] = 'counted'; |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
class TypedArrayPropertyInvalidMapKey |
||||
{ |
||||
#[StdDict(Type::Bool, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
class TypedArrayPropertyMapAppend |
||||
{ |
||||
#[StdDict(Type::Int, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function typedArrayPropertyMapAppend(TypedArrayPropertyMapAppend $box): void |
||||
{ |
||||
$box->value[] = 'bad'; |
||||
} |
||||
@ -0,0 +1,8 @@ |
||||
<?php |
||||
#[Native] |
||||
class TypedArrayPropertyNativeValue {} |
||||
class TypedArrayPropertyNativeValueBox |
||||
{ |
||||
#[StdList(TypedArrayPropertyNativeValue::class)] |
||||
public array $values = []; |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
class TypedArrayPropertyNoArguments |
||||
{ |
||||
#[StdList] |
||||
public array $value = []; |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
class TypedArrayPropertyNonArrayProperty |
||||
{ |
||||
#[StdList(Type::Int)] |
||||
public string $value = ''; |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
<?php |
||||
class TypedArrayPropertyExpectedUser {} |
||||
class TypedArrayPropertyOtherUser {} |
||||
class TypedArrayPropertyClassMismatchBox |
||||
{ |
||||
#[StdList(TypedArrayPropertyExpectedUser::class)] |
||||
public array $users = []; |
||||
} |
||||
function typedArrayPropertyStaticClassMismatch(TypedArrayPropertyClassMismatchBox $box): void |
||||
{ |
||||
$box->users[] = new TypedArrayPropertyOtherUser(); |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
class TypedArrayPropertyStaticKeyMismatch |
||||
{ |
||||
#[StdDict(Type::Int, Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function typedArrayPropertyStaticKeyMismatch(TypedArrayPropertyStaticKeyMismatch $box): void |
||||
{ |
||||
$box->value['bad'] = 'value'; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
#[Native] |
||||
class TypedArrayPropertyStaticValueMismatch |
||||
{ |
||||
#[StdList(Type::String)] |
||||
public array $value = []; |
||||
} |
||||
function typedArrayPropertyStaticValueMismatch(TypedArrayPropertyStaticValueMismatch $box): void |
||||
{ |
||||
$box->value[] = 123; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
class TypedArrayPropertyStdContainerValueBox |
||||
{ |
||||
#[StdList(Type::Int)] |
||||
public array $values = []; |
||||
} |
||||
function typedArrayPropertyStdContainerValue(TypedArrayPropertyStdContainerValueBox $box): void |
||||
{ |
||||
$values = std::vector(Type::Int); |
||||
$box->values[] = $values; |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
class TypedArrayPropertyTooManyArguments |
||||
{ |
||||
#[StdDict(Type::Int, Type::String, Type::Bool)] |
||||
public array $value = []; |
||||
} |
||||
@ -1,39 +0,0 @@ |
||||
<?php |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
final class ArrayDefTest extends \BaseTest |
||||
{ |
||||
public function testArrayDefDeclarationAndDirectWriteDiagnostics(): void |
||||
{ |
||||
$this->exec('ArrayDef can only be applied to properties declared as array', 'array-def-non-array-property.php'); |
||||
$this->exec('ArrayDef expects one or two type arguments', 'array-def-no-arguments.php'); |
||||
$this->exec('ArrayDef expects one or two type arguments', 'array-def-too-many-arguments.php'); |
||||
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-invalid-map-key.php'); |
||||
$this->exec('ArrayDef map keys must use Type::Int or Type::String', 'array-def-class-map-key.php'); |
||||
$this->exec('ArrayDef map properties do not support append writes', 'array-def-map-append.php'); |
||||
$this->exec('expects key of type int, string given', 'array-def-static-key-mismatch.php'); |
||||
$this->exec('expects value of type string, int given', 'array-def-static-value-mismatch.php'); |
||||
$this->exec('expects value of type ArrayDefExpectedUser, ArrayDefOtherUser given', 'array-def-static-class-mismatch.php'); |
||||
$this->exec('Native class types cannot be used in ArrayDef', 'array-def-native-class-value.php'); |
||||
$this->exec('Std Container values cannot be stored in ArrayDef properties', 'array-def-std-container-value.php'); |
||||
} |
||||
|
||||
public function testListIndexUsesPhpAppendBoundaryWithoutAstSpecialCase(): void |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/array-def-inclusive-upper-bound.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
self::assertSame(2, substr_count($code, 'php::safeArrayIndex(')); |
||||
self::assertStringNotContainsString('.length() + 1', $code); |
||||
self::assertStringNotContainsString('.newItem()', $code); |
||||
} |
||||
} |
||||
@ -0,0 +1,113 @@ |
||||
<?php |
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider; |
||||
use TypePhp\CompilerTest; |
||||
use TypePhp\Exception\TestError; |
||||
use TypePhp\Type; |
||||
|
||||
final class StdAttributeTypeTest extends BaseTest |
||||
{ |
||||
private function translate(string $source): array |
||||
{ |
||||
global $translator; |
||||
$directory = sys_get_temp_dir() . '/std-attribute-type-' . bin2hex(random_bytes(6)); |
||||
mkdir($directory); |
||||
$file = $directory . '/declarations.php'; |
||||
file_put_contents($file, '<?php ' . $source); |
||||
try { |
||||
$translator = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator->addFiles([$file]); |
||||
$translator->prepareFile($file); |
||||
return [file_get_contents($translator->convertFile($file)), $translator]; |
||||
} finally { |
||||
unlink($file); |
||||
rmdir($directory); |
||||
} |
||||
} |
||||
|
||||
public function testMatchingParameterStorageTypes(): void |
||||
{ |
||||
[$code, $compiler] = $this->translate(<<<'PHP' |
||||
function vector_arg(#[StdVector(Type::Int)] box $v): void { $v[] = 1; } |
||||
function map_arg(#[StdMap(Type::Str, Type::Int)] box $v): void { $v['a'] = 2; } |
||||
function ordered_arg(#[StdOrderedMap(Type::Int, Type::Str)] box $v): void { $v[0] = 'a'; } |
||||
function list_arg(#[StdList(Type::Int)] array $v): void { $v[] = 3; } |
||||
function dict_arg(#[StdDict(Type::Str, Type::Int)] array &$v): void { $v['a'] = 4; } |
||||
PHP); |
||||
self::assertStringContainsString('php_vector_arg(php::Var v)', $code); |
||||
self::assertStringContainsString('php_list_arg(php::Array v)', $code); |
||||
self::assertStringContainsString('php_dict_arg(php::Array & v)', $code); |
||||
$getFunction = new ReflectionMethod($compiler, 'getFunction'); |
||||
self::assertSame(Type::VAR, $getFunction->invoke($compiler, 'vector_arg')->argInfoList[0]->type); |
||||
self::assertSame(Type::ARRAY, $getFunction->invoke($compiler, 'list_arg')->argInfoList[0]->type); |
||||
self::assertSame(Type::ARRAY_REF, $getFunction->invoke($compiler, 'dict_arg')->argInfoList[0]->type); |
||||
} |
||||
|
||||
public function testPropertyAnnotationsAndSparseWrites(): void |
||||
{ |
||||
[$code, $compiler] = $this->translate(<<<'PHP' |
||||
class User { public int $id = 1; } |
||||
class State { |
||||
#[StdList(Type::Int)] public array $values = []; |
||||
#[StdDict(Type::Str, User::class)] public array $users = []; |
||||
#[StdList(Type::Str)] public $inferred = []; |
||||
#[StdDict(Type::Int, Type::Str)] public static array $labels = []; |
||||
#[StdVector(Type::Int)] public box $vector; |
||||
#[StdMap(Type::Str, Type::Int)] public box $map; |
||||
#[StdOrderedMap(Type::Int, User::class)] public $ordered; |
||||
} |
||||
#[Native] class NativeState { #[StdList(Type::Int)] public $values = []; } |
||||
function write_values(State $s, NativeState $n, $key): void { |
||||
$s->values[-2] = 10; |
||||
$s->values[100] = 20; |
||||
$s->values[] = 30; |
||||
$s->values[$key] = 40; |
||||
$s->users['123'] = new User(); |
||||
$s->inferred[100] = 'sparse'; |
||||
State::$labels[-3] = 'negative'; |
||||
$n->values[-5] = 7; |
||||
} |
||||
PHP); |
||||
self::assertStringNotContainsString('safeArrayIndex(', $code); |
||||
self::assertStringContainsString('php::toIntExact(', $code); |
||||
$class = (new ReflectionMethod($compiler, 'getClass'))->invoke($compiler, 'State'); |
||||
self::assertSame(Type::ARRAY, $class->getProperty('inferred')->type); |
||||
self::assertSame('list', $class->getProperty('values')->typedArray['kind']); |
||||
self::assertSame('dict', $class->getProperty('users')->typedArray['kind']); |
||||
self::assertSame('User', $class->getProperty('users')->typedArray['class']); |
||||
self::assertSame('vector', $class->getProperty('vector')->stdContainer['kind']); |
||||
self::assertSame('ordered_map', $class->getProperty('ordered')->stdContainer['kind']); |
||||
self::assertSame(Type::BOX, $class->getProperty('ordered')->type); |
||||
} |
||||
|
||||
#[DataProvider('conflictingDeclarations')] |
||||
public function testConflictingDeclarations(string $source, string $message): void |
||||
{ |
||||
try { |
||||
$this->translate($source); |
||||
self::fail('Conflicting container declaration was accepted'); |
||||
} catch (TestError|\TypePhp\Exception\SyntaxError $error) { |
||||
self::assertStringContainsString($message, $error->getMessage()); |
||||
} |
||||
} |
||||
|
||||
public static function conflictingDeclarations(): iterable |
||||
{ |
||||
foreach (['StdVector(Type::Int)', 'StdMap(Type::Str, Type::Int)', 'StdOrderedMap(Type::Int, Type::Str)'] as $attribute) { |
||||
foreach (['array', 'mixed', 'any', '?box', 'box|int'] as $type) { |
||||
yield $attribute . ' parameter ' . $type => ["function f(#[{$attribute}] {$type} \$v): void {}", 'unless it is box']; |
||||
yield $attribute . ' property ' . $type => ["class A { #[{$attribute}] public {$type} \$v; }", 'unless it is box']; |
||||
} |
||||
} |
||||
foreach (['StdList(Type::Int)', 'StdDict(Type::Str, Type::Int)'] as $attribute) { |
||||
foreach (['box', 'mixed', 'any', '?array', 'array|int'] as $type) { |
||||
yield $attribute . ' parameter ' . $type => ["function f(#[{$attribute}] {$type} \$v): void {}", 'unless it is array']; |
||||
yield $attribute . ' property ' . $type => ["class A { #[{$attribute}] public {$type} \$v; }", 'unless it is array']; |
||||
} |
||||
} |
||||
yield 'property container conflict' => ['class A { #[StdVector(Type::Int), StdList(Type::Int)] public $v; }', 'cannot be applied to the same declaration']; |
||||
yield 'property wrong list key' => ['class A { #[StdList(Type::Int)] public array $v = []; } function f(A $a): void { $a->v["1"] = 1; }', 'Typed array key must have type']; |
||||
yield 'property wrong list value' => ['class A { #[StdList(Type::Int)] public array $v = []; } function f(A $a): void { $a->v[] = "1"; }', 'Typed array value must have type']; |
||||
yield 'property dict append' => ['class A { #[StdDict(Type::Int, Type::Int)] public array $v = []; } function f(A $a): void { $a->v[] = 1; }', 'StdDict properties do not support append']; |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
<?php |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
final class TypedArrayPropertyTest extends \BaseTest |
||||
{ |
||||
public function testPropertyDeclarationAndDirectWriteDiagnostics(): void |
||||
{ |
||||
$this->exec('unless it is array', 'typed-array-property-non-array-property.php'); |
||||
$this->exec('expects 1 type argument', 'typed-array-property-no-arguments.php'); |
||||
$this->exec('expects 2 type argument', 'typed-array-property-too-many-arguments.php'); |
||||
$this->exec('key only supports Type::Int or Type::String', 'typed-array-property-invalid-map-key.php'); |
||||
$this->exec('key only supports Type::Int or Type::String', 'typed-array-property-class-map-key.php'); |
||||
$this->exec('StdDict properties do not support append writes', 'typed-array-property-map-append.php'); |
||||
$this->exec('Typed array key must have type', 'typed-array-property-static-key-mismatch.php'); |
||||
$this->exec('Typed array value must have type', 'typed-array-property-static-value-mismatch.php'); |
||||
$this->exec('Typed array value must be an instance of TypedArrayPropertyExpectedUser', 'typed-array-property-static-class-mismatch.php'); |
||||
$this->exec('Typed PHP arrays cannot hold Native objects', 'typed-array-property-native-class-value.php'); |
||||
$this->exec('Typed array value must have type', 'typed-array-property-std-container-value.php'); |
||||
} |
||||
|
||||
public function testListIndexAllowsSparseAndNegativeKeysWithoutBoundsChecks(): void |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/typed-array-property-inclusive-upper-bound.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
self::assertStringNotContainsString('php::safeArrayIndex(', $code); |
||||
self::assertStringNotContainsString('.length() + 1', $code); |
||||
self::assertStringNotContainsString('.newItem()', $code); |
||||
} |
||||
} |
||||
@ -0,0 +1,169 @@ |
||||
<?php |
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider; |
||||
use TypePhp\CompilerTest; |
||||
use TypePhp\Exception\TestError; |
||||
|
||||
final class TypedArrayTest extends BaseTest |
||||
{ |
||||
private function translate(string $source, bool $varIntTypes = false): array |
||||
{ |
||||
global $translator; |
||||
$directory = sys_get_temp_dir() . '/typephp-typed-array-' . bin2hex(random_bytes(6)); |
||||
mkdir($directory); |
||||
$file = $directory . '/typed.php'; |
||||
file_put_contents($file, '<?php ' . ($varIntTypes ? 'use varint_types; ' : '') |
||||
. str_replace('function main(', 'function probe(', $source)); |
||||
try { |
||||
$translator = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator->addFiles([$file]); |
||||
$translator->prepareFile($file); |
||||
$generated = $translator->convertFile($file); |
||||
return [file_get_contents($generated), $translator]; |
||||
} finally { |
||||
unlink($file); |
||||
rmdir($directory); |
||||
} |
||||
} |
||||
|
||||
public function testStoragePropagationAndNativeReferences(): void |
||||
{ |
||||
[$code, $compiler] = $this->translate(<<<'PHP' |
||||
function copy_values(#[StdList(Type::Int)] $values): void { $values[] = 99; } |
||||
function append_values(#[StdList(Type::Int)] &$values): void { $values[] = 42; } |
||||
class User { public int $id = 1; } |
||||
class Receiver { |
||||
public function accept(#[StdDict(Type::Str, User::class)] &$users): void { $users['alice'] = new User(); } |
||||
} |
||||
function main(): void { |
||||
$values = std::list(Type::Int); |
||||
$values[] = 7; |
||||
$copy = $values; |
||||
$alias =& $values; |
||||
$alias[] = 8; |
||||
copy_values($copy); |
||||
append_values(values: $alias); |
||||
var_dump(array_search(7, $values), array_keys($values), count($copy)); |
||||
$users = std::dict(Type::Str, User::class); |
||||
$receiver = new Receiver(); |
||||
$receiver->accept($users); |
||||
$user = $users['alice']; |
||||
var_dump($user->id); |
||||
} |
||||
PHP); |
||||
self::assertStringContainsString('php::Array &', $code); |
||||
self::assertStringContainsString('php::Array{}', $code); |
||||
self::assertStringNotContainsString('StdContainerBox', $code); |
||||
self::assertStringNotContainsString('toIntExact', $code); |
||||
self::assertStringNotContainsString('toObjectExact', $code); |
||||
self::assertStringNotContainsString('ZEND_FUNCTION(copy_values)', $code); |
||||
self::assertTrue($compiler->isNativeFunctionForStub('copy_values')); |
||||
self::assertTrue($compiler->isNativeMethodForStub('Receiver', 'accept')); |
||||
$function = (new ReflectionMethod($compiler, 'getFunction'))->invoke($compiler, 'append_values'); |
||||
$parameter = $function->argInfoList[0]; |
||||
self::assertSame(\TypePhp\Type::ARRAY_REF, $parameter->type); |
||||
self::assertSame($parameter->typedArray, unserialize(serialize($parameter))->typedArray); |
||||
} |
||||
|
||||
public function testWideningIntegerExpressionRequiresExplicitConversion(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('may widen require an explicit'); |
||||
$this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = $value + 1; }', true); |
||||
} |
||||
|
||||
public function testExplicitNativeIntegerConversionInVarIntMode(): void |
||||
{ |
||||
[$code] = $this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = std::int($value + 1); }', true); |
||||
self::assertStringContainsString('.appendValue(', $code); |
||||
} |
||||
|
||||
public function testExistingIntegerKeywordConversionInVarIntMode(): void |
||||
{ |
||||
[$code] = $this->translate('function foo(int $value): void { $list = std::list(Type::Int); $list[] = ($value + 1)->toInt(); }', true); |
||||
self::assertStringContainsString('php::toInt(', $code); |
||||
self::assertStringNotContainsString('toIntExact(', $code); |
||||
} |
||||
|
||||
public function testDynamicKeysUseInternalStrictChecks(): void |
||||
{ |
||||
[$code] = $this->translate(<<<'PHP' |
||||
function foo($key): void { |
||||
$list = std::list(Type::Int); |
||||
$list[$key] = 1; |
||||
var_dump($list[$key], isset($list[$key]), array_key_exists($key, $list)); |
||||
unset($list[$key]); |
||||
$dict = std::dict(Type::Str, Type::Int); |
||||
$dict[$key] = 2; |
||||
var_dump($dict[$key], $dict->keyExists($key), $dict->get($key)); |
||||
unset($dict[$key]); |
||||
} |
||||
PHP); |
||||
self::assertStringContainsString('php::toIntExact(', $code); |
||||
self::assertStringContainsString('php::toStringExact(', $code); |
||||
self::assertStringNotContainsString('toExactInt(', $code); |
||||
} |
||||
|
||||
#[DataProvider('invalidDeclarations')] |
||||
public function testInvalidContractsAndEscapes(string $code, string $message): void |
||||
{ |
||||
try { |
||||
$this->translate($code); |
||||
self::fail('Expected compilation failure'); |
||||
} catch (TestError|\TypePhp\Exception\SyntaxError $error) { |
||||
self::assertStringContainsString($message, $error->getMessage()); |
||||
} |
||||
} |
||||
|
||||
public static function invalidDeclarations(): iterable |
||||
{ |
||||
yield 'wrong value' => ['function main(): void { $a = std::list(Type::Int); $a[] = "x"; }', 'must have type']; |
||||
yield 'dynamic value' => ['function main($value): void { $a = std::list(Type::Int); $a[] = $value; }', 'must have type']; |
||||
yield 'wrong key' => ['function main(): void { $a = std::dict(Type::Int, Type::Str); $a["x"] = "v"; }', 'must have type']; |
||||
yield 'list string key' => ['function main(): void { $a = std::list(Type::Int); $a["x"] = 1; }', 'must have type']; |
||||
yield 'string dict append' => ['function main(): void { $a = std::dict(Type::Str, Type::Int); $a[] = 1; }', 'Only typed lists']; |
||||
yield 'integer dict append' => ['function main(): void { $a = std::dict(Type::Int, Type::Int); $a[] = 1; }', 'Only typed lists']; |
||||
yield 'missing list type' => ['function main(): void { $a = std::list(); }', 'expects 1 type']; |
||||
yield 'invalid dict key' => ['function main(): void { $a = std::dict(Type::Bool, Type::Int); }', 'key only supports']; |
||||
yield 'annotation plus mixed' => ['function foo(#[StdList(Type::Int)] mixed $a): void {}', 'a PHP type cannot also']; |
||||
yield 'duplicate annotation' => ['function foo(#[StdList(Type::Int), StdList(Type::Int)] $a): void {}', 'cannot be repeated']; |
||||
yield 'conflicting annotation' => ['function foo(#[StdList(Type::Int), StdVector(Type::Int)] $a): void {}', 'cannot be applied']; |
||||
yield 'named type argument' => ['function foo(#[StdList(valueType: Type::Int)] $a): void {}', 'positional type']; |
||||
yield 'wrong argument type' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = std::list(Type::Float); foo($a); }', 'identical list/dict contract']; |
||||
yield 'list and dict remain distinct' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = std::dict(Type::Int, Type::Int); foo($a); }', 'identical list/dict contract']; |
||||
yield 'untyped array argument' => ['function foo(#[StdList(Type::Int)] $a): void {} function main(): void { $a = []; foo($a); }', 'identical list/dict contract']; |
||||
yield 'unannotated function' => ['function foo(array $a): void {} function main(): void { $a = std::list(Type::Int); foo($a); }', 'matching annotated']; |
||||
yield 'unannotated reference' => ['function foo(array &$a): void {} function main(): void { $a = std::list(Type::Int); foo($a); }', 'matching annotated']; |
||||
yield 'array push' => ['function main(): void { $a = std::list(Type::Int); array_push($a, 1); }', 'matching annotated']; |
||||
yield 'array sort' => ['function main(): void { $a = std::list(Type::Int); sort($a); }', 'matching annotated']; |
||||
yield 'array walk' => ['function main(): void { $a = std::list(Type::Int); array_walk($a, "var_dump"); }', 'matching annotated']; |
||||
yield 'dynamic ref wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $callback(std::ref($a)); }', 'cannot escape']; |
||||
yield 'dynamic element wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $a[] = 1; $callback(std::ref($a[0])); }', 'cannot escape']; |
||||
yield 'dynamic toRef wrapper' => ['function main($callback): void { $a = std::list(Type::Int); $callback($a->toRef()); }', 'cannot escape']; |
||||
yield 'element reference' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $v =& $a[0]; }', 'cannot escape']; |
||||
yield 'reference return' => ['function &foo(#[StdList(Type::Int)] &$a) { return $a; }', 'cannot escape']; |
||||
yield 'element ref assignment' => ['function main(): void { $a = std::list(Type::Int); $v = 1; $a[] =& $v; }', 'cannot escape']; |
||||
yield 'property reference escape' => ['class A { public array $values = []; } function main(): void { $a = std::list(Type::Int); $object = new A(); $object->values =& $a; }', 'cannot escape']; |
||||
yield 'wrong copied write' => ['function main(): void { $a = std::list(Type::Int); $b = $a; $b[] = "x"; }', 'must have type']; |
||||
yield 'wrong alias write' => ['function main(): void { $a = std::list(Type::Int); $b =& $a; $b[] = "x"; }', 'must have type']; |
||||
yield 'plain array replacement' => ['function main(): void { $a = std::list(Type::Int); $a = []; }', 'identical list/dict contract']; |
||||
yield 'dynamic array replacement' => ['function main($other): void { $a = std::list(Type::Int); $a = $other; }', 'identical list/dict contract']; |
||||
yield 'incompatible object' => ['class A {} class B {} function main(): void { $a = std::list(A::class); $a[] = new B(); }', 'must be an instance']; |
||||
yield 'unknown object' => ['class A {} function main(object $value): void { $a = std::list(A::class); $a[] = $value; }', 'must be an instance']; |
||||
yield 'native object' => ['#[Native] class A {} function main(): void { $a = std::list(A::class); }', 'cannot hold Native']; |
||||
yield 'closure contract' => ['$fn = function(#[StdList(Type::Int)] $a) {};', 'named function or method']; |
||||
yield 'arrow contract' => ['$fn = fn(#[StdList(Type::Int)] $a) => 1;', 'named function or method']; |
||||
yield 'reference closure capture' => ['function main(): void { $a = std::list(Type::Int); $fn = function() use (&$a) {}; }', 'cannot be captured by reference']; |
||||
yield 'typed value closure capture' => ['function main(): void { $a = std::list(Type::Int); $fn = function() use ($a) { $a[] = "x"; }; $fn(); }', 'must have type']; |
||||
yield 'foreach ref' => ['function main(): void { $a = std::list(Type::Int); foreach ($a as &$v) {} }', 'cannot escape']; |
||||
yield 'incompatible override' => ['class A { public function foo(#[StdList(Type::Int)] $a): void {} } class B extends A { public function foo(#[StdList(Type::Float)] $a): void {} }', 'must be compatible']; |
||||
yield 'array reference escape' => ['function main(): void { $a = std::list(Type::Int); $bundle = [&$a]; }', 'cannot escape']; |
||||
yield 'compound write' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $a[0] += "x"; }', 'compound writes']; |
||||
yield 'coalescing write' => ['function main(): void { $a = std::list(Type::Int); $a[0] ??= "x"; }', 'compound writes']; |
||||
yield 'union write' => ['function main(): void { $a = std::list(Type::Int); $a += ["x"]; }', 'compound writes']; |
||||
yield 'increment write' => ['function main(): void { $a = std::list(Type::Int); $a[] = 1; $a[0]++; }', 'increment/decrement']; |
||||
yield 'universal mutator' => ['function main(): void { $a = std::list(Type::Int); $a->set(0, "x"); }', 'untyped mutating']; |
||||
yield 'foreach value type' => ['function main(): void { $a = std::list(Type::Int); foreach ($a as $value) { $value = "x"; } }', 'Cannot re-assign']; |
||||
yield 'foreach key type' => ['function main(): void { $a = std::dict(Type::Str, Type::Int); foreach ($a as $key => $value) { $key = 1; } }', 'Cannot re-assign']; |
||||
} |
||||
} |
||||
@ -1,255 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\ArrayDef; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\NodeAbstract; |
||||
use TypePhp\Entity\PropertyDef; |
||||
use TypePhp\Transform\CompileTimeAttribute; |
||||
use TypePhp\Type; |
||||
|
||||
/** |
||||
* Compile-time-only ArrayDef declaration parsing and direct-write checking. |
||||
* |
||||
* This deliberately does not attempt to guard array values that escape into |
||||
* ZendVM calls. ArrayDef is a static contract for syntax the compiler owns. |
||||
*/ |
||||
trait ArrayDefSupportTrait |
||||
{ |
||||
protected function parseArrayDefinition(Node\Stmt\Property|Node\Param $property): ?ArrayDefinition |
||||
{ |
||||
$attribute = CompileTimeAttribute::find($property, 'ArrayDef'); |
||||
if ($attribute === null) { |
||||
return null; |
||||
} |
||||
|
||||
if (!$property->type instanceof Node\Identifier |
||||
|| strtolower($property->type->toString()) !== 'array' |
||||
) { |
||||
$this->fatalError($property, 'ArrayDef can only be applied to properties declared as array'); |
||||
} |
||||
|
||||
$count = count($attribute->args); |
||||
if ($count < 1 || $count > 2) { |
||||
$this->fatalError($attribute, 'ArrayDef expects one or two type arguments'); |
||||
} |
||||
|
||||
$types = []; |
||||
foreach ($attribute->args as $arg) { |
||||
if ($arg->name !== null || !$arg->value instanceof Expr\ClassConstFetch) { |
||||
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||
} |
||||
$class = $arg->value->class; |
||||
$constant = $arg->value->name; |
||||
if (!$class instanceof Node\Name || !$constant instanceof Node\Identifier) { |
||||
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||
} |
||||
$resolved = $class->getAttribute('resolvedName') ?? $class; |
||||
$resolvedClass = ltrim($resolved->toString(), '\\'); |
||||
if (strcasecmp($constant->toString(), 'class') === 0) { |
||||
if ($this->isNativeObjectClass($resolvedClass)) { |
||||
$this->fatalError($arg, 'Native class types cannot be used in ArrayDef'); |
||||
} |
||||
$types[] = $resolvedClass; |
||||
continue; |
||||
} |
||||
if (strcasecmp($resolvedClass, 'Type') !== 0) { |
||||
$this->fatalError($arg, 'ArrayDef arguments must be Type::* or ClassName::class constants'); |
||||
} |
||||
$types[] = $this->resolveArrayDefType($constant->toString(), $arg); |
||||
} |
||||
|
||||
if ($count === 1) { |
||||
return new ArrayDefinition(null, $types[0]); |
||||
} |
||||
if (!in_array($types[0], [Type::INT, Type::STR], true)) { |
||||
$this->fatalError($attribute, 'ArrayDef map keys must use Type::Int or Type::String'); |
||||
} |
||||
return new ArrayDefinition($types[0], $types[1]); |
||||
} |
||||
|
||||
private function resolveArrayDefType(string $name, NodeAbstract $errorNode): string |
||||
{ |
||||
$type = match (strtolower($name)) { |
||||
'int' => Type::INT, |
||||
'float' => Type::FLOAT, |
||||
'bool' => Type::BOOL, |
||||
'string' => Type::STR, |
||||
'array' => Type::ARRAY, |
||||
'object' => Type::OBJECT, |
||||
'any' => Type::VAR, |
||||
'stream' => Type::STREAM, |
||||
'bigint' => Type::BIGINT, |
||||
'bigfloat' => Type::BIGFLOAT, |
||||
'decimal' => Type::DECIMAL, |
||||
default => null, |
||||
}; |
||||
if ($type === null) { |
||||
$this->fatalError($errorNode, "Unsupported ArrayDef type Type::{$name}"); |
||||
} |
||||
return $type; |
||||
} |
||||
|
||||
protected function prepareArrayDefDirectWrite( |
||||
Expr\ArrayDimFetch $left, |
||||
Expr $right, |
||||
string $value, |
||||
): ?ArrayDefWritePlan |
||||
{ |
||||
$def = $this->getNativePropertyDef($left->var); |
||||
$arrayDef = $def?->arrayDef; |
||||
if ($arrayDef === null) { |
||||
return null; |
||||
} |
||||
|
||||
$value = $this->validateArrayDefWriteValue($left->var, $right, $value, $arrayDef->valueType, 'value'); |
||||
if ($left->dim === null) { |
||||
if (!$arrayDef->isList()) { |
||||
$this->fatalError($left, 'ArrayDef map properties do not support append writes'); |
||||
} |
||||
return new ArrayDefWritePlan(true, null, $value); |
||||
} |
||||
|
||||
$expectedKey = $arrayDef->keyType ?? Type::INT; |
||||
$key = $this->parseExprAsValue($left->dim); |
||||
$key = $this->validateArrayDefWriteValue($left->var, $left->dim, $key, $expectedKey, 'key'); |
||||
if ($arrayDef->isList()) { |
||||
$array = $this->parseWritableIdentifier($left->var); |
||||
// PHP's append index does not shrink after unset(). Element count |
||||
// is therefore not a valid list-write boundary for sparse arrays. |
||||
$key = 'php::safeArrayIndex(' . $key . ', ' . $array . ')'; |
||||
} |
||||
|
||||
return new ArrayDefWritePlan(false, $key, $value); |
||||
} |
||||
|
||||
private function validateArrayDefWriteValue( |
||||
NodeAbstract $property, |
||||
Expr $expr, |
||||
string $code, |
||||
string $expected, |
||||
string $part, |
||||
): string |
||||
{ |
||||
$actual = $this->detectTypeOfExpr($expr); |
||||
$stdContainerValue = $this->isVarExpr($expr) |
||||
&& $this->isStdContainer($this->parseIdentifier($expr)); |
||||
if ($part === 'value' && ($stdContainerValue || $this->isStdContainerType($actual))) { |
||||
$this->fatalError($expr, 'Std Container values cannot be stored in ArrayDef properties'); |
||||
} |
||||
if ($expected === Type::VAR) { |
||||
return $code; |
||||
} |
||||
|
||||
if ($this->isArrayDefClassType($expected)) { |
||||
$actualClass = $this->detectClassOfExpr($expr); |
||||
if ($actualClass !== '') { |
||||
if (!$this->isObjectClassStaticallyAssignableTo($actualClass, $expected)) { |
||||
$this->fatalError( |
||||
$expr, |
||||
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||
. ' expects ' . $part . ' of type ' . $expected |
||||
. ', ' . $actualClass . ' given', |
||||
); |
||||
} |
||||
return $code; |
||||
} |
||||
|
||||
if ($actual !== Type::VAR && $actual !== Type::OBJECT) { |
||||
$this->fatalError( |
||||
$expr, |
||||
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||
. ' expects ' . $part . ' of type ' . $expected |
||||
. ', ' . $this->arrayDefTypeName($actual) . ' given', |
||||
); |
||||
} |
||||
return 'php::toObjectExact(' |
||||
. $code . ', ' . $this->getClassEntryPtr($expected) . ', ' |
||||
. $this->genCharPtr( |
||||
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part, |
||||
true, |
||||
) |
||||
. ')'; |
||||
} |
||||
|
||||
if ($actual !== Type::VAR) { |
||||
if ($actual !== $expected) { |
||||
$this->fatalError( |
||||
$expr, |
||||
'ArrayDef property ' . $this->getObjectPropertyTypeCheckDisplayName($property) |
||||
. ' expects ' . $part . ' of type ' . $this->arrayDefTypeName($expected) |
||||
. ', ' . $this->arrayDefTypeName($actual) . ' given', |
||||
); |
||||
} |
||||
return $code; |
||||
} |
||||
|
||||
$display = $this->genCharPtr( |
||||
$this->getObjectPropertyTypeCheckDisplayName($property) . ' ArrayDef ' . $part, |
||||
true, |
||||
); |
||||
$helper = match ($expected) { |
||||
Type::INT => 'php::toIntExact', |
||||
Type::FLOAT => 'php::toFloatExact', |
||||
Type::BOOL => 'php::toBoolExact', |
||||
Type::STR => 'php::toStringExact', |
||||
Type::ARRAY => 'php::toArrayExact', |
||||
Type::OBJECT => 'php::toObjectExact', |
||||
Type::STREAM => 'php::toStreamExact', |
||||
Type::BIGINT => 'php::toBoxExact<php::BigInt>', |
||||
Type::BIGFLOAT => 'php::toBoxExact<php::BigFloat>', |
||||
Type::DECIMAL => 'php::toBoxExact<php::Decimal>', |
||||
default => null, |
||||
}; |
||||
if ($helper === null) { |
||||
return $code; |
||||
} |
||||
$boxType = in_array($expected, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true) |
||||
? ', ' . $this->genCharPtr($this->arrayDefTypeName($expected), true) |
||||
: ''; |
||||
return $helper . '(' . $code . ', ' . $display . $boxType . ')'; |
||||
} |
||||
|
||||
private function arrayDefTypeName(string $type): string |
||||
{ |
||||
return match ($type) { |
||||
Type::INT => 'int', |
||||
Type::FLOAT => 'float', |
||||
Type::BOOL => 'bool', |
||||
Type::STR => 'string', |
||||
Type::ARRAY => 'array', |
||||
Type::OBJECT => 'object', |
||||
Type::STREAM => 'stream', |
||||
Type::BIGINT => 'BigInt', |
||||
Type::BIGFLOAT => 'BigFloat', |
||||
Type::DECIMAL => 'Decimal', |
||||
Type::VAR => 'any', |
||||
default => $type, |
||||
}; |
||||
} |
||||
|
||||
private function isArrayDefClassType(string $type): bool |
||||
{ |
||||
return !in_array($type, [ |
||||
Type::INT, |
||||
Type::FLOAT, |
||||
Type::BOOL, |
||||
Type::STR, |
||||
Type::ARRAY, |
||||
Type::OBJECT, |
||||
Type::VAR, |
||||
Type::STREAM, |
||||
Type::BIGINT, |
||||
Type::BIGFLOAT, |
||||
Type::DECIMAL, |
||||
], true); |
||||
} |
||||
|
||||
} |
||||
@ -1,19 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\ArrayDef; |
||||
|
||||
final readonly class ArrayDefWritePlan |
||||
{ |
||||
public function __construct( |
||||
public bool $append, |
||||
public ?string $key, |
||||
public string $value, |
||||
) { |
||||
} |
||||
} |
||||
@ -1,23 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\ArrayDef; |
||||
|
||||
final readonly class ArrayDefinition |
||||
{ |
||||
public function __construct( |
||||
public ?string $keyType, |
||||
public string $valueType, |
||||
) { |
||||
} |
||||
|
||||
public function isList(): bool |
||||
{ |
||||
return $this->keyType === null; |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
namespace TypePhp\Entity; |
||||
|
||||
final readonly class TypedArrayPropertyWritePlan |
||||
{ |
||||
public function __construct( |
||||
public bool $append, |
||||
public ?string $key, |
||||
public string $value, |
||||
) { |
||||
} |
||||
} |
||||
@ -0,0 +1,419 @@ |
||||
<?php |
||||
|
||||
namespace TypePhp\Parser; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Stmt\Foreach_; |
||||
use PhpParser\NodeAbstract; |
||||
use TypePhp\Entity\ArgInfo; |
||||
use TypePhp\Entity\TypedArrayPropertyWritePlan; |
||||
use TypePhp\Transform\CompileTimeAttribute; |
||||
use TypePhp\Type; |
||||
|
||||
/** Compiler-enforced PHP-array contracts. Storage and COW remain php::Array's. */ |
||||
trait TypedArrayTrait |
||||
{ |
||||
protected function parseTypedArrayPropertyDefinition(Node\Stmt\Property $property): ?array |
||||
{ |
||||
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) { |
||||
$attribute = CompileTimeAttribute::find($property, $name); |
||||
if ($attribute !== null) { |
||||
$this->validateStdAttributeType($property, $name, 'array'); |
||||
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected function prepareTypedArrayPropertyDirectWrite( |
||||
Expr\ArrayDimFetch $left, |
||||
Expr $right, |
||||
string $value, |
||||
): ?TypedArrayPropertyWritePlan { |
||||
$definition = $this->getNativePropertyDef($left->var)?->typedArray; |
||||
if ($definition === null) { |
||||
return null; |
||||
} |
||||
$value = $this->guardTypedArrayValue($definition, $right, $value); |
||||
if ($left->dim === null) { |
||||
if ($definition['kind'] !== 'list') { |
||||
$this->fatalError($left, 'StdDict properties do not support append writes'); |
||||
} |
||||
return new TypedArrayPropertyWritePlan(true, null, $value); |
||||
} |
||||
$key = $this->guardTypedArrayValue($definition, $left->dim, $this->parseExprAsValue($left->dim), true); |
||||
return new TypedArrayPropertyWritePlan(false, $key, $value); |
||||
} |
||||
|
||||
protected function parseTypedArrayDefinition(string $kind, array $args, NodeAbstract $owner): array |
||||
{ |
||||
$count = $kind === 'list' ? 1 : 2; |
||||
if (count($args) !== $count) { |
||||
$this->fatalError($owner, "Std{$kind} expects {$count} type argument(s)"); |
||||
} |
||||
foreach ($args as $arg) { |
||||
if ($arg->name !== null || $arg->unpack || $arg->byRef) { |
||||
$this->fatalError($arg, 'Typed arrays require positional type arguments'); |
||||
} |
||||
} |
||||
$key = $kind === 'list' ? Type::INT : $this->parseStdMapKeyType($args[0]->value, 'std::dict'); |
||||
$value = $this->parseStdValueTypeInfo($args[$count - 1]->value, 'std::' . $kind); |
||||
if (!in_array($value['type'], [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY, Type::OBJECT, Type::VAR], true)) { |
||||
$this->fatalError($owner, 'Typed PHP arrays only support PHP value types or ClassName::class'); |
||||
} |
||||
if ($this->isNativeObjectClass($value['class'] ?? '')) { |
||||
$this->fatalError($owner, 'Typed PHP arrays cannot hold Native objects'); |
||||
} |
||||
// Integer-key lists/dicts share PHP storage; only list grants append. |
||||
return ['kind' => $kind, 'keyType' => $key, 'type' => $value['type'], 'class' => $value['class']]; |
||||
} |
||||
|
||||
protected function parseTypedArrayParameterDefinition(Node\Param $param): ?array |
||||
{ |
||||
foreach (['StdList' => 'list', 'StdDict' => 'dict'] as $name => $kind) { |
||||
$attribute = CompileTimeAttribute::find($param, $name); |
||||
if ($attribute === null) { |
||||
continue; |
||||
} |
||||
$this->validateStdAttributeType($param, $name, 'array'); |
||||
if ($param->variadic || $param->default !== null || $param->isPromoted()) { |
||||
$this->fatalError($param, $name . ' does not support variadic, defaulted or promoted parameters'); |
||||
} |
||||
return $this->parseTypedArrayDefinition($kind, $attribute->args, $attribute); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected function getTypedArrayDefinition(NodeAbstract $expr): ?array |
||||
{ |
||||
if ($expr instanceof Expr\ErrorSuppress) { |
||||
return $this->getTypedArrayDefinition($expr->expr); |
||||
} |
||||
if ($expr instanceof Expr\Assign || $expr instanceof Expr\AssignRef) { |
||||
return $this->getTypedArrayDefinition($expr->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)) { |
||||
return $this->parseTypedArrayDefinition(strtolower($expr->name->name), $expr->args, $expr); |
||||
} |
||||
if ($this->isVarExpr($expr)) { |
||||
return $this->context->typedArrays[$this->parseIdentifier($expr)] ?? null; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected function getTypedArrayAccessDefinition(NodeAbstract $expr): ?array |
||||
{ |
||||
return $expr instanceof Expr\ArrayDimFetch ? $this->getTypedArrayDefinition($expr->var) : null; |
||||
} |
||||
|
||||
protected function assertTypedArrayReferenceForbidden(NodeAbstract $expr): void |
||||
{ |
||||
while ($expr instanceof Expr\ArrayDimFetch) { |
||||
$expr = $expr->var; |
||||
} |
||||
if ($this->getTypedArrayDefinition($expr) !== null) { |
||||
$this->fatalError($expr, 'Typed arrays cannot escape through std::ref(), toRef(), or element references'); |
||||
} |
||||
} |
||||
|
||||
protected function parseTypedArrayAssignment(Expr $left, Expr $right): ?string |
||||
{ |
||||
if ($this->getTypedArrayAccessDefinition($left) !== null) { |
||||
return $this->parseTypedArrayWrite($left, $right); |
||||
} |
||||
// Nested writes would bypass the declared outer element type. |
||||
if ($left instanceof Expr\ArrayDimFetch && $left->var instanceof Expr\ArrayDimFetch |
||||
&& $this->getTypedArrayAccessDefinition($left->var) !== null) { |
||||
$this->fatalError($left, 'Nested typed-array writes must replace a checked element'); |
||||
} |
||||
if (!$this->isVarExpr($left)) { |
||||
return null; |
||||
} |
||||
$name = $this->parseWritableIdentifier($left); |
||||
$factory = $right instanceof Expr\StaticCall && $right->class instanceof Node\Name |
||||
&& $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->getTypedArrayDefinition($right); |
||||
$existing = $this->context->typedArrays[$name] ?? null; |
||||
if ($definition === null && $existing === null) { |
||||
return null; |
||||
} |
||||
if ($existing !== null && $definition !== $existing) { |
||||
$this->fatalError($right, 'Typed array assignment requires an identical list/dict contract'); |
||||
} |
||||
if ($this->hasScopeGlobalVar($name) || $this->hasStaticVar($name) |
||||
|| isset($this->context->varTypeDegradations[$name])) { |
||||
$this->fatalError($left, 'Typed arrays require fixed function-local storage and cannot be captured by reference'); |
||||
} |
||||
if ($factory && $this->hasVar($name)) { |
||||
$this->fatalError($left, 'Typed array factories require a new variable'); |
||||
} |
||||
if ($existing === null) { |
||||
if ($this->hasVar($name) && $this->getRawVarType($name) !== Type::ARRAY) { |
||||
$this->fatalError($left, 'Cannot erase a typed array into dynamic or reference storage'); |
||||
} |
||||
// Do not retrofit a contract onto an already-live ordinary array: |
||||
// existing aliases or previous paths may still write untyped values. |
||||
if ($this->hasVar($name)) { |
||||
$this->fatalError($left, 'Typed array propagation requires a new variable'); |
||||
} |
||||
$this->addLocalVar($name, Type::ARRAY); |
||||
$this->context->typedArrays[$name] = $definition; |
||||
} |
||||
return $name . ' = ' . ($factory ? 'php::Array{}' : $this->parseExprAsValue($right)); |
||||
} |
||||
|
||||
protected function guardTypedArrayValue(array $def, Expr $expr, string $code, bool $key = false): string |
||||
{ |
||||
$this->assertExprCanBeUsedAsValue($expr, 'typed array element'); |
||||
$expected = $key ? $def['keyType'] : $def['type']; |
||||
$class = $key ? '' : ($def['class'] ?? ''); |
||||
$actual = Type::getReferencedType($this->detectTypeOfExpr($expr)); |
||||
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) { |
||||
$this->fatalError($expr, 'Native objects cannot be stored in typed PHP arrays'); |
||||
} |
||||
if ($key && $actual === Type::VAR) { |
||||
// Dynamic keys need a strict check, not a coercing conversion. |
||||
// These are internal PHPX helpers, not user keyword methods. |
||||
return ($expected === Type::INT ? 'php::toIntExact(' : 'php::toStringExact(') . $code . ')'; |
||||
} |
||||
if (!$key && $expected === Type::VAR) { |
||||
return $code; |
||||
} |
||||
if ($class !== '') { |
||||
$actualClass = $this->detectClassOfExpr($expr); |
||||
if ($actualClass === '' || !$this->isObjectClassStaticallyAssignableTo($actualClass, $class)) { |
||||
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class); |
||||
} |
||||
if ($actual !== Type::OBJECT) { |
||||
$this->fatalError($expr, 'Typed array value must be an instance of ' . $class); |
||||
} |
||||
return $code; |
||||
} |
||||
if ($actual !== $expected) { |
||||
$this->fatalError($expr, 'Typed array ' . ($key ? 'key' : 'value') . ' must have type ' . $expected); |
||||
} |
||||
if ($expected === Type::INT && $this->varIntTypes && $this->exprCanOverflowInt($expr)) { |
||||
$this->fatalError($expr, 'Typed array integer expressions that may widen require an explicit toInt() or native integer conversion'); |
||||
} |
||||
return $code; |
||||
} |
||||
|
||||
protected function convertTypedArrayRead(array $def, string $code): string |
||||
{ |
||||
// Recover the statically proven type; no Exact checks or element scans. |
||||
return $this->convertExprFromType($def['type'], $code); |
||||
} |
||||
|
||||
protected function parseTypedArrayKey(Expr\ArrayDimFetch $expr, bool $write = false): string |
||||
{ |
||||
$def = $this->getTypedArrayAccessDefinition($expr); |
||||
if ($expr->dim === null) { |
||||
if (!$write || $def['kind'] !== 'list') { |
||||
$this->fatalError($expr, 'Only typed lists support append writes'); |
||||
} |
||||
return ''; |
||||
} |
||||
$key = $this->guardTypedArrayValue($def, $expr->dim, $this->parseExprAsValue($expr->dim), true); |
||||
return $key; |
||||
} |
||||
|
||||
protected function parseTypedArrayRead(Expr\ArrayDimFetch $expr): string |
||||
{ |
||||
$def = $this->getTypedArrayAccessDefinition($expr); |
||||
return $this->convertTypedArrayRead($def, $this->parseTypedArrayRawRead($expr)); |
||||
} |
||||
|
||||
protected function parseTypedArrayRawRead(Expr\ArrayDimFetch $expr): string |
||||
{ |
||||
$array = $this->parseIdentifier($expr->var); |
||||
$key = $this->parseTypedArrayKey($expr); |
||||
return $array . '.offsetGet(' . $key . ')'; |
||||
} |
||||
|
||||
protected function parseTypedArrayPresence(Expr\ArrayDimFetch $expr, string $op, bool $getValue): string |
||||
{ |
||||
$raw = $this->parseTypedArrayRawRead($expr); |
||||
if ($getValue) { |
||||
$result = $this->addTmpVar(Type::VAR); |
||||
$expr->setAttribute('chainOpResult', $result); |
||||
$raw = '(' . $result . ' = ' . $raw . ')'; |
||||
} |
||||
return match ($op) { |
||||
self::OP_ISSET => 'php::exists(' . $raw . ')', |
||||
self::OP_EMPTY => 'php::empty(' . $raw . ')', |
||||
self::OP_NOT_EMPTY => 'php::notEmpty(' . $raw . ')', |
||||
default => $this->getChainedFunc($op) . '(' . $raw . ')', |
||||
}; |
||||
} |
||||
|
||||
protected function parseTypedArrayKeyExistsCall(Expr\FuncCall $call): ?string |
||||
{ |
||||
if (count($call->args) !== 2) { |
||||
return null; |
||||
} |
||||
$arguments = []; |
||||
foreach ($call->args as $index => $argument) { |
||||
if ($argument->unpack || $argument->byRef) { |
||||
return null; |
||||
} |
||||
$name = $argument->name?->name ?? ($index === 0 ? 'key' : 'array'); |
||||
if (!in_array($name, ['key', 'array'], true) || isset($arguments[$name])) { |
||||
return null; |
||||
} |
||||
$arguments[$name] = $argument->value; |
||||
} |
||||
if (!isset($arguments['key'], $arguments['array']) |
||||
|| $this->getTypedArrayDefinition($arguments['array']) === null) { |
||||
return null; |
||||
} |
||||
$definition = $this->getTypedArrayDefinition($arguments['array']); |
||||
// Snapshot source arguments in order, including named arguments. |
||||
$values = []; |
||||
foreach ($arguments as $name => $argument) { |
||||
if ($name === 'key') { |
||||
$key = $this->guardTypedArrayValue($definition, $argument, $this->parseExprAsValue($argument), true); |
||||
$values[$name] = $this->addTmpVar($definition['keyType']); |
||||
$this->context->beforeStmtLines[] = $values[$name] . ' = ' . $key . ';'; |
||||
} else { |
||||
$values[$name] = $this->parseOrderedOperand($argument, false, true); |
||||
} |
||||
} |
||||
// Existing PHPX lookup retains PHP numeric-string normalization. |
||||
return $values['array'] . '.exists(' . $values['key'] . ')'; |
||||
} |
||||
|
||||
protected function parseTypedArrayForeach(Foreach_ $node, array $def): string |
||||
{ |
||||
if ($node->byRef) { |
||||
$this->fatalError($node, 'Typed array elements cannot escape through foreach references'); |
||||
} |
||||
$array = $this->parseIdentifier($node->expr); |
||||
$iterator = $this->genTmpVarName(); |
||||
$assignments = ''; |
||||
foreach ([[$node->keyVar, $def['keyType'], '', 'key'], |
||||
[$node->valueVar, $def['type'], $def['class'] ?? '', 'value']] as [$target, $type, $class, $part]) { |
||||
if ($target === null) { |
||||
continue; |
||||
} |
||||
if (!$this->isVarExpr($target)) { |
||||
$this->fatalError($target, 'Typed array foreach requires simple typed key/value variables'); |
||||
} |
||||
$this->assertImmutableMutationTarget($target); |
||||
$name = $this->parseWritableIdentifier($target); |
||||
if (isset($this->context->varTypeDegradations[$name])) { |
||||
$this->fatalError($target, 'Typed foreach variables cannot be captured by reference'); |
||||
} |
||||
if ($this->hasVar($name) && ($this->getRawVarType($name) !== $type |
||||
|| ($class !== '' && $this->getDeclaredObjectType($name) !== $class))) { |
||||
$this->fatalError($target, 'Typed array foreach variable must have the declared key/value type'); |
||||
} |
||||
if (!$this->hasVar($name)) { |
||||
$this->addLocalVar($name, $type); |
||||
if ($class !== '') { |
||||
$this->addObject($name, $class); |
||||
} |
||||
} |
||||
// PHP normalizes numeric string keys to integers. Recover the |
||||
// declared key type here, without changing PHPX or array storage. |
||||
$assignments .= $this->getIndent() . ' ' . $name . ' = ' |
||||
. $this->convertExprFromType($type, $iterator . '.' . $part . '()') . ';' . PHP_EOL; |
||||
} |
||||
$scope = $this->class ? $this->getLocalClassEntryPtr($this->getFullClassName()) : 'nullptr'; |
||||
$code = '{' . PHP_EOL . $this->getIndent() . 'php::ForeachIterator ' . $iterator |
||||
. '{' . $array . ', false, ' . $scope . '};' . PHP_EOL |
||||
. $this->getIndent() . 'while (' . $iterator . '.next()) {' . PHP_EOL . $assignments; |
||||
$this->indentLevel++; |
||||
$body = $this->parseForeachBody($node); |
||||
$this->indentLevel--; |
||||
return $code . $this->parseBeforeStmtLines() . $body . $this->getIndent() . '}' . PHP_EOL |
||||
. $this->getIndent() . '}' . PHP_EOL; |
||||
} |
||||
|
||||
protected function parseTypedArrayWrite(Expr\ArrayDimFetch $left, Expr $right): string |
||||
{ |
||||
$def = $this->getTypedArrayAccessDefinition($left); |
||||
$array = $this->parseWritableIdentifier($left->var); |
||||
$key = $this->parseTypedArrayKey($left, true); |
||||
// Snapshot a key before lowering a RHS that might hoist side effects. |
||||
if ($key !== '') { |
||||
$keyVar = $this->addTmpVar($def['keyType']); |
||||
$this->context->beforeStmtLines[] = $keyVar . ' = ' . $key . ';'; |
||||
$key = $keyVar; |
||||
} |
||||
$value = $this->guardTypedArrayValue($def, $right, $this->parseExprAsValue($right)); |
||||
$result = $this->addTmpVar(Type::VAR); |
||||
// An assignment expression yields the assigned value, not void. |
||||
return '[&]() -> php::Var { ' . $result . ' = ' . $value . '; ' |
||||
. $this->genTypedArrayStore($array, $key, $result) . '; return ' . $result . '; }()'; |
||||
} |
||||
|
||||
private function genTypedArrayStore(string $array, string $key, string $value): string |
||||
{ |
||||
if ($key === '') { |
||||
return $array . '.appendValue(' . $value . ')'; |
||||
} |
||||
return $array . '.offsetSet(' . $key . ', ' . $value . ')'; |
||||
} |
||||
|
||||
protected function parseTypedArrayUnset(Expr\ArrayDimFetch $expr): string |
||||
{ |
||||
$array = $this->parseWritableIdentifier($expr->var); |
||||
$key = $this->parseTypedArrayKey($expr); |
||||
return $array . '.offsetUnset(' . $key . ')'; |
||||
} |
||||
|
||||
protected function assertTypedArrayArgument(Node\Arg $arg, ?ArgInfo $parameter, bool $byRef, bool $project): void |
||||
{ |
||||
$expr = $arg->value; |
||||
if ($this->isReferenceWrapperCall($expr)) { |
||||
$expr = $this->unwrapReferenceWrapperCall($expr, $arg); |
||||
$this->assertTypedArrayReferenceForbidden($expr); |
||||
} |
||||
$def = $this->getTypedArrayDefinition($expr); |
||||
$expected = $parameter?->typedArray; |
||||
if ($expected !== null && $def !== $expected) { |
||||
$this->fatalError($arg, 'Typed array parameter requires an identical list/dict contract'); |
||||
} |
||||
if ($def !== null && ($arg->unpack || ($project && $expected === null) || ($byRef && $expected === null))) { |
||||
$this->fatalError($arg, 'Typed arrays require matching annotated TypePHP parameters or read-only PHP calls'); |
||||
} |
||||
if ($byRef && $expected === null) { |
||||
$this->assertTypedArrayReferenceForbidden($expr); |
||||
} |
||||
} |
||||
|
||||
protected function validateTypedArrayDynamicArgument(Node\Arg $arg, string $function, string $class, int $index): void |
||||
{ |
||||
$parameter = $arg->name !== null |
||||
? $this->getAotCallArgInfoByName($function, $class, $arg->name->name) |
||||
: $this->getAotCallArgInfo($function, $class, $index); |
||||
if ($parameter?->typedArray !== null) { |
||||
$this->fatalError($arg, 'Typed array parameters require a statically resolved native TypePHP call, not Zend dispatch'); |
||||
} |
||||
$byRef = $function !== '' && ($arg->name !== null |
||||
? $this->isReferenceNamedArgument($function, $class, $arg->name->name) |
||||
: $this->isReferenceArgument($function, $class, $index)); |
||||
$this->assertTypedArrayArgument($arg, $parameter, $byRef, $parameter !== null); |
||||
} |
||||
|
||||
protected function functionUsesTypedArray(\TypePhp\Entity\FunctionDef $function): bool |
||||
{ |
||||
foreach ($function->argInfoList as $parameter) { |
||||
if ($parameter->typedArray !== null) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function functionRequiresNativeAbi(\TypePhp\Entity\FunctionDef $function): bool |
||||
{ |
||||
return $this->functionUsesNativeObject($function) || $this->functionUsesTypedArray($function); |
||||
} |
||||
} |
||||
@ -1,84 +0,0 @@ |
||||
--TEST-- |
||||
ArrayDef checks dynamic compound and high-precision value types |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class ArrayDefCompoundBox |
||||
{ |
||||
#[ArrayDef(Type::Array)] |
||||
public array $arrays = []; |
||||
|
||||
#[ArrayDef(Type::Object)] |
||||
public array $objects = []; |
||||
|
||||
#[ArrayDef(Type::BigInt)] |
||||
public array $bigInts = []; |
||||
|
||||
#[ArrayDef(Type::BigFloat)] |
||||
public array $bigFloats = []; |
||||
|
||||
#[ArrayDef(Type::Decimal)] |
||||
public array $decimals = []; |
||||
} |
||||
|
||||
function putCompound(ArrayDefCompoundBox $box, int $slot, any $value): void |
||||
{ |
||||
if ($slot === 0) { |
||||
$box->arrays[] = $value; |
||||
} elseif ($slot === 1) { |
||||
$box->objects[] = $value; |
||||
} elseif ($slot === 2) { |
||||
$box->bigInts[] = $value; |
||||
} elseif ($slot === 3) { |
||||
$box->bigFloats[] = $value; |
||||
} else { |
||||
$box->decimals[] = $value; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$box = new ArrayDefCompoundBox(); |
||||
putCompound($box, 0, [1, 2]); |
||||
putCompound($box, 1, new stdClass()); |
||||
putCompound($box, 2, std::bigInt(3)); |
||||
putCompound($box, 3, std::bigFloat('4')); |
||||
putCompound($box, 4, std::decimal('5')); |
||||
|
||||
var_dump($box->arrays[0]); |
||||
var_dump($box->objects[0] instanceof stdClass); |
||||
echo std::bigInt($box->bigInts[0])->toString(), "\n"; |
||||
echo std::bigFloat($box->bigFloats[0])->toString(), "\n"; |
||||
echo std::decimal($box->decimals[0])->toString(), "\n"; |
||||
|
||||
try { |
||||
putCompound($box, 0, 'not-array'); |
||||
} catch (TypeError $error) { |
||||
echo "array checked\n"; |
||||
} |
||||
try { |
||||
putCompound($box, 1, []); |
||||
} catch (TypeError $error) { |
||||
echo "object checked\n"; |
||||
} |
||||
try { |
||||
putCompound($box, 2, std::decimal('6')); |
||||
} catch (TypeError $error) { |
||||
echo "BigInt checked\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(2) |
||||
} |
||||
bool(true) |
||||
3 |
||||
4 |
||||
5 |
||||
array checked |
||||
object checked |
||||
BigInt checked |
||||
@ -0,0 +1,71 @@ |
||||
--TEST-- |
||||
Std attributes accept matching array/box storage types and sparse property list writes |
||||
--FILE-- |
||||
<?php |
||||
class PropertyState |
||||
{ |
||||
#[StdList(Type::Int)] public array $values = []; |
||||
#[StdDict(Type::Str, Type::Int)] public array $counts = []; |
||||
#[StdList(Type::Str)] public $names = []; |
||||
#[StdDict(Type::Int, Type::Str)] public static array $labels = []; |
||||
#[StdVector(Type::Int)] public box $vector; |
||||
} |
||||
#[Native] |
||||
class NativePropertyState |
||||
{ |
||||
#[StdList(Type::Int)] public $values = []; |
||||
} |
||||
function append_vector(#[StdVector(Type::Int)] box $v): void { $v[] = 5; } |
||||
function write_map(#[StdMap(Type::Str, Type::Int)] box $m): void { $m['a'] = 6; } |
||||
function write_ordered(#[StdOrderedMap(Type::Int, Type::Str)] box $m): void { $m[1] = 'one'; } |
||||
function copy_list(#[StdList(Type::Int)] array $a): void { $a[] = 7; } |
||||
function append_list(#[StdList(Type::Int)] array &$a): void { $a[] = 8; } |
||||
function write_dict(#[StdDict(Type::Str, Type::Int)] array &$a): void { $a['a'] = 9; } |
||||
function write_property(PropertyState $s, $key): void { $s->values[$key] = 40; } |
||||
function main(): void |
||||
{ |
||||
$v = std::vector(Type::Int); |
||||
append_vector($v); |
||||
echo $v[0], "\n"; |
||||
$m = std::map(Type::Str, Type::Int); |
||||
write_map($m); |
||||
echo $m['a'], "\n"; |
||||
$o = std::orderedMap(Type::Int, Type::Str); |
||||
write_ordered($o); |
||||
echo $o[1], "\n"; |
||||
$a = std::list(Type::Int); |
||||
copy_list($a); |
||||
append_list($a); |
||||
echo count($a), ':', $a[0], "\n"; |
||||
$d = std::dict(Type::Str, Type::Int); |
||||
write_dict($d); |
||||
echo $d['a'], "\n"; |
||||
$s = new PropertyState(); |
||||
$s->values[-5] = 10; |
||||
$s->values[100] = 20; |
||||
$s->values[] = 30; |
||||
write_property($s, 200); |
||||
echo $s->values[-5], ':', $s->values[100], ':', $s->values[101], ':', $s->values[200], "\n"; |
||||
try { write_property($s, '200'); } |
||||
catch (TypeError $error) { echo "strict property key\n"; } |
||||
$s->counts['123'] = 2; |
||||
$s->names[100] = 'sparse'; |
||||
PropertyState::$labels[-3] = 'negative'; |
||||
echo $s->counts['123'], ':', $s->names[100], ':', PropertyState::$labels[-3], "\n"; |
||||
$n = new NativePropertyState(); |
||||
$n->values[-2] = 11; |
||||
$n->values[100] = 12; |
||||
$n->values[] = 13; |
||||
echo $n->values[-2], ':', $n->values[100], ':', $n->values[101], "\n"; |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
5 |
||||
6 |
||||
one |
||||
1:8 |
||||
9 |
||||
10:20:30:40 |
||||
strict property key |
||||
2:sparse:negative |
||||
11:12:13 |
||||
@ -0,0 +1,57 @@ |
||||
--TEST-- |
||||
Typed PHP arrays strictly check dynamic keys without changing PHPX or coercing keys |
||||
--FILE-- |
||||
<?php |
||||
function main(): void |
||||
{ |
||||
$list = std::list(Type::Int); |
||||
$key = std::any(50); |
||||
$list[$key] = 7; |
||||
var_dump($list[$key], isset($list[$key]), empty($list[$key])); |
||||
var_dump(array_key_exists($key, $list), $list->keyExists($key), $list->get($key)); |
||||
unset($list[$key]); |
||||
var_dump(isset($list[$key]), count($list)); |
||||
|
||||
$dict = std::dict(Type::Str, Type::Int); |
||||
$name = std::any('123'); |
||||
$dict[$name] = 9; |
||||
var_dump($dict[$name], key_exists(array: $dict, key: $name), $dict->get($name)); |
||||
foreach ($dict as $stringKey => $value) { |
||||
var_dump(is_string($stringKey), $stringKey, $value); |
||||
} |
||||
|
||||
try { $list[std::any('50')] = 1; } catch (TypeError $error) { echo "write rejected\n"; } |
||||
try { var_dump($dict[std::any(123)]); } catch (TypeError $error) { echo "read rejected\n"; } |
||||
try { var_dump(isset($list[std::any(true)])); } catch (TypeError $error) { echo "isset rejected\n"; } |
||||
try { unset($dict[std::any(1.5)]); } catch (TypeError $error) { echo "unset rejected\n"; } |
||||
try { var_dump(array_key_exists(std::any('50'), $list)); } catch (TypeError $error) { echo "key exists rejected\n"; } |
||||
try { var_dump($dict->get(std::any(123))); } catch (TypeError $error) { echo "get rejected\n"; } |
||||
var_dump(count($list), count($dict)); |
||||
unset($dict[$name]); |
||||
var_dump(count($dict)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(7) |
||||
bool(true) |
||||
bool(false) |
||||
bool(true) |
||||
bool(true) |
||||
int(7) |
||||
bool(false) |
||||
int(0) |
||||
int(9) |
||||
bool(true) |
||||
int(9) |
||||
bool(true) |
||||
string(3) "123" |
||||
int(9) |
||||
write rejected |
||||
read rejected |
||||
isset rejected |
||||
unset rejected |
||||
key exists rejected |
||||
get rejected |
||||
int(0) |
||||
int(1) |
||||
int(0) |
||||
@ -0,0 +1,46 @@ |
||||
--TEST-- |
||||
Typed array explicit key conversions, readonly calls and dynamic by-value COW isolation |
||||
--FILE-- |
||||
<?php |
||||
function dynamic_mutator(array &$array): void { $array[] = 'wrong'; } |
||||
function main(): void |
||||
{ |
||||
$list = std::list(Type::Int); |
||||
$key = std::any(50); |
||||
$list[$key->toInt()] = 7; |
||||
$list[] = 8; |
||||
var_dump($list[50], array_search(8, $list, true), count(array_keys($list))); |
||||
$callback = 'dynamic_mutator'; |
||||
// Zend warns about the unknown reference signature; it receives a value |
||||
// snapshot, never a reference to the statically typed array. |
||||
@$callback($list); |
||||
$alias =& $list; |
||||
@$callback($alias); |
||||
var_dump(count($list)); |
||||
$dict = std::dict(Type::Str, Type::Int); |
||||
$stringKey = std::any('123'); |
||||
$dict[$stringKey->toString()] = 9; |
||||
var_dump(array_keys($dict), $dict['123']); |
||||
var_dump(array_key_exists('123', $dict), key_exists(array: $dict, key: '123')); |
||||
var_dump($dict->keyExists('123'), $dict->get('123'), $dict->keyExists('missing')); |
||||
$list[std::any('50')->toInt()] = 10; |
||||
var_dump($list[50], count($list)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(7) |
||||
int(51) |
||||
int(2) |
||||
int(2) |
||||
array(1) { |
||||
[0]=> |
||||
int(123) |
||||
} |
||||
int(9) |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
int(9) |
||||
bool(false) |
||||
int(10) |
||||
int(2) |
||||
@ -0,0 +1,55 @@ |
||||
--TEST-- |
||||
Typed list/dict class values propagate through foreach and native method parameters |
||||
--FILE-- |
||||
<?php |
||||
use StdList as ListOf; |
||||
class TypedUser |
||||
{ |
||||
public function __construct(public int $id) {} |
||||
public function name(): string { return 'user'; } |
||||
} |
||||
class TypedReceiver |
||||
{ |
||||
public function fill(#[StdDict(Type::Str, TypedUser::class)] &$users): void |
||||
{ |
||||
$users['123'] = new TypedUser(9); |
||||
$users['alice'] = new TypedUser(7); |
||||
} |
||||
} |
||||
function show(#[ListOf(TypedUser::class)] $users): void |
||||
{ |
||||
foreach ($users as $key => $user) { |
||||
echo $key, ':', $user->id, ':', $user->name(), "\n"; |
||||
} |
||||
} |
||||
function main(): void |
||||
{ |
||||
$list = std::list(TypedUser::class); |
||||
$list[] = new TypedUser(1); |
||||
show($list); |
||||
$dict = std::dict(Type::Str, TypedUser::class); |
||||
$receiver = new TypedReceiver(); |
||||
$receiver->fill($dict); |
||||
$copy = $dict; |
||||
foreach ($dict as $name => $user) { |
||||
var_dump(is_string($name), $name, $user->id); |
||||
} |
||||
var_dump($dict['123']->id, isset($dict['123']), empty($dict['123'])); |
||||
unset($dict['123']); |
||||
var_dump(isset($dict['123']), isset($copy['123']), $copy['123']->id); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
0:1:user |
||||
bool(true) |
||||
string(3) "123" |
||||
int(9) |
||||
bool(true) |
||||
string(5) "alice" |
||||
int(7) |
||||
int(9) |
||||
bool(true) |
||||
bool(false) |
||||
bool(false) |
||||
bool(true) |
||||
int(9) |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
StdList and StdDict properties support statically typed PHP array/object/any values |
||||
--FILE-- |
||||
<?php |
||||
class PhpValueProperties |
||||
{ |
||||
#[StdList(Type::Array)] public array $arrays = []; |
||||
#[StdList(Type::Object)] public array $objects = []; |
||||
#[StdDict(Type::Str, Type::Any)] public array $values = []; |
||||
} |
||||
function putArray(PhpValueProperties $s, array $value): void { $s->arrays[] = $value; } |
||||
function putObject(PhpValueProperties $s, object $value): void { $s->objects[] = $value; } |
||||
function main(): void |
||||
{ |
||||
$s = new PhpValueProperties(); |
||||
putArray($s, [1, 2]); |
||||
putObject($s, new stdClass()); |
||||
$s->values['int'] = 3; |
||||
$s->values['str'] = 'four'; |
||||
var_dump($s->arrays[0], $s->objects[0] instanceof stdClass, $s->values['int'], $s->values['str']); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(2) |
||||
} |
||||
bool(true) |
||||
int(3) |
||||
string(4) "four" |
||||
@ -0,0 +1,62 @@ |
||||
--TEST-- |
||||
Typed PHP arrays retain COW storage, native references, sparse keys and foreach types |
||||
--FILE-- |
||||
<?php |
||||
function copy_list(#[StdList(Type::Int)] $list): void |
||||
{ |
||||
$list[] = 99; |
||||
} |
||||
function append_list(#[StdList(Type::Int)] &$list): void |
||||
{ |
||||
$list[] = 42; |
||||
} |
||||
function main(): void |
||||
{ |
||||
$list = std::list(Type::Int); |
||||
$list[-3] = 5; |
||||
$list[100] = 7; |
||||
$list[] = 8; |
||||
$copy = $list; |
||||
$alias =& $list; |
||||
$alias2 =& $alias; |
||||
$alias2[100] = 9; |
||||
copy_list($list); |
||||
append_list(list: $alias); |
||||
var_dump(is_array($list), count($list), $list[100], $copy[100]); |
||||
foreach ($list as $key => $value) { |
||||
echo $key, ':', $value, "\n"; |
||||
} |
||||
$dict = std::dict(Type::Int, Type::Str); |
||||
$dict[-10] = 'minus'; |
||||
$dict[1000] = 'sparse'; |
||||
foreach ($dict as $id => $name) { |
||||
echo $id, ':', $name, "\n"; |
||||
} |
||||
unset($list[100]); |
||||
var_dump(isset($list[100]), empty($list[100]), count($alias)); |
||||
var_dump(array_search(8, $list), array_sum($list)); |
||||
$visit = function() use ($list): int { |
||||
$list[] = 70; |
||||
return count($list); |
||||
}; |
||||
var_dump($visit(), count($list)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
int(4) |
||||
int(9) |
||||
int(7) |
||||
-3:5 |
||||
100:9 |
||||
101:8 |
||||
102:42 |
||||
-10:minus |
||||
1000:sparse |
||||
bool(false) |
||||
bool(true) |
||||
int(3) |
||||
int(101) |
||||
int(55) |
||||
int(4) |
||||
int(3) |
||||
Loading…
Reference in new issue