- 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