diff --git a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md index 305a5279..467ac0c0 100644 --- a/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md +++ b/docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md @@ -78,6 +78,8 @@ | 动态魔术方法、变量属性/方法名不支持 | Native magic/dynamic access deny-list | dynamic magic、variable method/property 负向测试 | 已验证 | | `toArray/toString/toInt/toFloat/toBool` 要求实体方法和精确返回类型 | Native keyword method resolution | `keyword-conversions.phpt` 及 missing/wrong return 负向测试 | 已验证 | | `count($obj)` 仅在实现 Countable 时特化 | Native count optimizer | `keyword-conversions.phpt`、count-without-countable 负向测试 | 已验证 | +| `ArrayAccess` 直接语法映射到 Native `offset*()` 方法 | Native array access lowering | `array-access.phpt` | 已验证 | +| Native `ArrayAccess` 禁止间接修改和引用 | writable-chain/reference validators | ArrayAccess compound/increment/nested/property/reference/coalesce 负向测试 | 已验证 | ## 6. GC 与生命周期 @@ -127,6 +129,6 @@ vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php --gtest_filter='wren_gc.*:native_gc.*' ``` -本次结果分别为:69/69 PHPT、131/131 PHPUnit、17/17 PHPX C++ tests。 +本次结果分别为:70/70 PHPT、137/137 PHPUnit、17/17 PHPX C++ tests。 完整回归结果为:编译器 PHPUnit 1431/1431、编译器 PHPT 1037/1037(另有 2 项按 环境跳过)、PHPX C++ tests 1016/1016。Native 分支的公共 hook 未影响普通对象模型。 diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index dbb1791f..7ddcc09c 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -1284,7 +1284,9 @@ $json = json_encode($nativeObject->toArray()); | 动态 callback | 不支持 | | 动态 PHP/eval 使用 | 不支持 | | 普通 PHP array 保存 Native Object | 不支持 | -| Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError | +| Native Object 作为 PHP array key | 不支持;编译期 FatalError | +| Native Object 作为 `[]` receiver | 实现 `ArrayAccess` 时支持直接读写、追加、`isset`、`empty`、`??` 和 `unset`;直接生成 Native `offset*()` 调用 | +| Native `ArrayAccess` 元素间接修改 | 不支持 `++/--`、复合赋值、`??=`、嵌套写入、属性写入和取引用;编译期 FatalError | | Box/Std Container 属性 | 不支持 | | Box 保存 Native Object | 不支持 | | 局部 Std Container 保存 Native Object | 仅支持函数顶层局部变量和具体 Native class value type;容器 Root Frame 参与 GC tracing | diff --git a/phpunit/code/native-class-array-access-coalesce-assign.php b/phpunit/code/native-class-array-access-coalesce-assign.php new file mode 100644 index 00000000..d62c9ef7 --- /dev/null +++ b/phpunit/code/native-class-array-access-coalesce-assign.php @@ -0,0 +1,16 @@ +property = 1; +} diff --git a/phpunit/code/native-class-array-access-reference.php b/phpunit/code/native-class-array-access-reference.php new file mode 100644 index 00000000..14ab7b9f --- /dev/null +++ b/phpunit/code/native-class-array-access-reference.php @@ -0,0 +1,16 @@ +compile('native-class-array-dim-key.php'); } - public function testRejectsArrayAccessOnNativeObjects(): void + public function testRejectsArrayAccessWithoutArrayAccessInterface(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax'); $this->compile('native-class-array-access.php'); } - public function testRejectsArrayWritesOnNativeObjects(): void + public function testRejectsArrayWritesWithoutArrayAccessInterface(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax'); $this->compile('native-class-array-access-write.php'); } - public function testRejectsArrayIssetOnNativeObjects(): void + public function testRejectsArrayIssetWithoutArrayAccessInterface(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax'); $this->compile('native-class-array-access-isset.php'); } - public function testRejectsArrayUnsetOnNativeObjects(): void + public function testRejectsArrayUnsetWithoutArrayAccessInterface(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native objects do not support array dimension access'); + $this->expectExceptionMessage('must implement `ArrayAccess` to use array access syntax'); $this->compile('native-class-array-access-unset.php'); } + public function testRejectsNativeArrayAccessCompoundModification(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported'); + $this->compile('native-class-array-access-compound.php'); + } + + public function testRejectsNativeArrayAccessIncrement(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported'); + $this->compile('native-class-array-access-increment.php'); + } + + public function testRejectsNativeArrayAccessNestedWrite(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported'); + $this->compile('native-class-array-access-nested-write.php'); + } + + public function testRejectsNativeArrayAccessPropertyWrite(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported'); + $this->compile('native-class-array-access-property-write.php'); + } + + public function testRejectsNativeArrayAccessReferences(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('References to Native ArrayAccess elements are not supported'); + $this->compile('native-class-array-access-reference.php'); + } + + public function testRejectsNativeArrayAccessCoalesceAssignment(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Indirect modification of Native ArrayAccess elements is not supported'); + $this->compile('native-class-array-access-coalesce-assign.php'); + } + public function testRejectsInaccessibleNativeCloneMethod(): void { $this->expectException(TestError::class); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 02705dd8..80ad89c2 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3202,6 +3202,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreInc(Expr\PreInc $expr): string { + $this->assertNativeArrayAccessDirectWrite($expr->var, false); $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '++'); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); @@ -3590,6 +3591,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string { + $this->assertNativeArrayAccessDirectWrite($expr->var, false); $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, str_repeat($op, 2)); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); @@ -3640,6 +3642,7 @@ class CompilerBase implements PropertyAccessContext protected function parsePreDec(Expr\PreDec $expr): string { + $this->assertNativeArrayAccessDirectWrite($expr->var, false); $this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '--'); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); @@ -4209,8 +4212,14 @@ class CompilerBase implements PropertyAccessContext protected function parseChainedExpr(NodeAbstract $node, string $op, bool $getValue = false): string { if ($op === self::OP_REFVAL) { + $this->assertNativeArrayAccessReferenceForbidden($node); $this->assertNativeObjectReferenceForbidden($node, $node); } + if ($node instanceof Expr\ArrayDimFetch + && $this->isNativeObjectClass($this->detectClassOfExpr($node->var)) + ) { + return $this->parseNativeArrayAccessPresence($node, $op, $getValue); + } if (in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) { $nativePresence = $this->parseNativeObjectPresenceChain($node, $op); if ($nativePresence !== null) { @@ -4266,6 +4275,10 @@ class CompilerBase implements PropertyAccessContext $list = []; while (true) { if ($this->isArrayDimFetch($expr)) { + if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->var))) { + $var = $this->parseArrayDimFetchRead($expr); + break; + } if ($expr->dim === null) { $this->fatalError($expr, 'Cannot use [] for reading'); } @@ -4310,6 +4323,72 @@ class CompilerBase implements PropertyAccessContext } } + /** + * Lower isset/empty/coalesce without exposing the Native pointer to the + * generic Variant chain walker. Repeated offsetExists/offsetGet operations + * share one receiver and key evaluation, matching PHP ArrayAccess order. + */ + protected function parseNativeArrayAccessPresence( + Expr\ArrayDimFetch $access, + string $op, + bool $getValue, + ): string { + if ($access->dim === null) { + $this->fatalError($access, 'Cannot use [] for reading'); + } + + if ($op === self::OP_REFVAL) { + $this->assertNativeArrayAccessReferenceForbidden($access); + } + if (!in_array($op, [self::OP_ISSET, self::OP_EMPTY, self::OP_NOT_EMPTY], true)) { + $this->fatalError($access, 'Unsupported Native ArrayAccess operation'); + } + + if ($op === self::OP_ISSET && !$getValue) { + return $this->parseNativeArrayAccessCall( + $access, + 'offsetExists', + [new Node\Arg($access->dim)], + ); + } + + $receiver = $access->var; + $class = $this->getNativeArrayAccessClass($receiver, $access); + if (!$this->isVarExpr($receiver)) { + $receiverName = $this->materializeNativeObjectReceiver($receiver, $class); + $receiver = new Expr\Variable($receiverName, $receiver->getAttributes()); + } + + $key = $this->addTmpVar(Type::VAR); + $keyExpr = $this->parseOrderedOperand($access->dim, false); + $this->context->beforeStmtLines[] = $key . ' = ' . $keyExpr . ';'; + $stableAccess = new Expr\ArrayDimFetch( + $receiver, + new Expr\Variable($key, $access->dim->getAttributes()), + $access->getAttributes(), + ); + $exists = $this->parseNativeArrayAccessCall( + $stableAccess, + 'offsetExists', + [new Node\Arg($stableAccess->dim)], + ); + $value = $this->parseNativeArrayAccessCall( + $stableAccess, + 'offsetGet', + [new Node\Arg($stableAccess->dim)], + ); + + if ($getValue) { + $result = $this->addTmpVar(Type::VAR); + $access->setAttribute('chainOpResult', $result); + return '(' . $exists . ' && ((' . $result . ' = ' . $value . '), true))'; + } + if ($op === self::OP_EMPTY) { + return '(!(' . $exists . ') || !php::notEmpty(' . $value . '))'; + } + return '(' . $exists . ' && php::notEmpty(' . $value . '))'; + } + /** * Lower a named Native property chain without converting its raw pointers * to Variant. The short-circuit lambda preserves PHP's isset()/empty() diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index bff3d2bb..330351f0 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -357,13 +357,107 @@ trait NativeClassSupportTrait } } - protected function assertNotNativeObjectArrayDimensionReceiver( + /** + * Return the statically known Native receiver class for array syntax. + * PHP only dispatches [] through ArrayAccess; having similarly named + * methods without the interface is not sufficient. + */ + protected function getNativeArrayAccessClass( NodeAbstract $receiver, NodeAbstract $errorNode, + ): ?string { + $class = $this->detectClassOfExpr($receiver); + if (!$this->isNativeObjectClass($class)) { + return null; + } + + $implementsArrayAccess = false; + foreach ($this->getClassImplementedInterfaces($this->getClass($class)) as $interface) { + if (strcasecmp(ltrim($interface, '\\'), 'ArrayAccess') === 0) { + $implementsArrayAccess = true; + break; + } + } + if (!$implementsArrayAccess) { + $this->fatalError( + $errorNode, + "Native class `{$class}` must implement `ArrayAccess` to use array access syntax", + ); + } + return $class; + } + + /** Locate the first Native ArrayAccess dimension inside a writable chain. */ + protected function findNativeArrayAccessDimension(NodeAbstract $expression): ?Node\Expr\ArrayDimFetch + { + if ($expression instanceof Node\Expr\ArrayDimFetch) { + if ($this->isNativeObjectClass($this->detectClassOfExpr($expression->var))) { + return $expression; + } + return $this->findNativeArrayAccessDimension($expression->var); + } + if ($expression instanceof Node\Expr\PropertyFetch + || $expression instanceof Node\Expr\NullsafePropertyFetch + ) { + return $this->findNativeArrayAccessDimension($expression->var); + } + return null; + } + + protected function assertNativeArrayAccessDirectWrite( + NodeAbstract $target, + bool $allowDirectDimension, ): void { - if ($this->isNativeObjectClass($this->detectClassOfExpr($receiver))) { - $this->fatalError($errorNode, 'Native objects do not support array dimension access'); + $dimension = $this->findNativeArrayAccessDimension($target); + if ($dimension === null) { + return; + } + $this->getNativeArrayAccessClass($dimension->var, $dimension); + if ($allowDirectDimension && $dimension === $target) { + return; } + $this->fatalError( + $target, + 'Indirect modification of Native ArrayAccess elements is not supported', + ); + } + + protected function assertNativeArrayAccessReferenceForbidden(NodeAbstract $expression): void + { + $dimension = $this->findNativeArrayAccessDimension($expression); + if ($dimension === null) { + return; + } + $this->getNativeArrayAccessClass($dimension->var, $dimension); + $this->fatalError( + $expression, + 'References to Native ArrayAccess elements are not supported', + ); + } + + /** + * Build a normal MethodCall so Native dispatch, visibility, signature + * checks, virtual thunks and PHP evaluation ordering remain centralized. + * + * @param list $arguments + */ + protected function parseNativeArrayAccessCall( + Node\Expr\ArrayDimFetch $access, + string $method, + array $arguments, + ): ?string { + if ($this->getNativeArrayAccessClass($access->var, $access) === null) { + return null; + } + if ($access->dim !== null) { + $this->assertNotNativeObjectArrayKey($access->dim); + } + return $this->parseMethodCall(new Node\Expr\MethodCall( + $access->var, + new Node\Identifier($method), + $arguments, + $access->getAttributes(), + )); } protected function assertNotNativeObjectDynamicClassTarget( diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 13281d98..ca7b7c50 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -110,6 +110,14 @@ trait ArrayExpressionTrait protected function parseWritableIdentifier(NodeAbstract $expr): string { if ($expr instanceof Expr\ArrayDimFetch) { + $dimension = $this->findNativeArrayAccessDimension($expr); + if ($dimension !== null) { + $this->getNativeArrayAccessClass($dimension->var, $dimension); + $this->fatalError( + $expr, + 'Indirect modification of Native ArrayAccess elements is not supported', + ); + } return $this->parseArrayDimFetchUpdate($expr); } @@ -174,7 +182,16 @@ trait ArrayExpressionTrait protected function parseArrayDimFetch(Expr\ArrayDimFetch $node): string { - $this->assertNotNativeObjectArrayDimensionReceiver($node->var, $node); + if ($this->isNativeObjectClass($this->detectClassOfExpr($node->var))) { + if ($node->dim === null) { + $this->fatalError($node, 'Cannot use [] for reading'); + } + return $this->parseNativeArrayAccessCall( + $node, + 'offsetGet', + [new Node\Arg($node->dim)], + ); + } if ($node->dim !== null) { $this->assertNotNativeObjectArrayKey($node->dim); } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 7018e6c7..6d74b3f2 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -24,9 +24,6 @@ trait AssignOpTrait if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) { $this->assertNotNativeObjectArrayKey($left->dim); } - if ($left instanceof Expr\ArrayDimFetch) { - $this->assertNotNativeObjectArrayDimensionReceiver($left->var, $left); - } if ($this->isPropertyFetch($left)) { return $this->parseAssignPropertyArrayDim($left, $right); } @@ -236,6 +233,45 @@ trait AssignOpTrait protected function parseAssignFinally(Expr $left, Expr $right): string { $this->assertNotNullsafeWriteContext($left); + $this->assertNativeArrayAccessDirectWrite($left, true); + if ($left instanceof Expr\ArrayDimFetch + && $this->isNativeObjectClass($this->detectClassOfExpr($left->var)) + ) { + if ($this->isNativeObjectClass($this->detectClassOfExpr($right))) { + $this->fatalError( + $right, + 'Native objects cannot cross a PHP/ZendVM argument boundary', + ); + } + $result = $this->addTmpVar(Type::VAR); + $key = $this->addTmpVar(Type::VAR); + $store = $this->parseNativeArrayAccessCall( + $left, + 'offsetSet', + [ + new Node\Arg(new Variable($key)), + new Node\Arg(new Variable($result)), + ], + ); + if ($left->dim === null) { + $keyExpr = self::VALUE_NULL; + $keyBefore = $keyAfter = []; + } else { + [$keyExpr, $keyBefore, $keyAfter] = $this->parseExprWithCapturedStmts($left->dim); + } + [$rightExpr, $rightBefore, $rightAfter] = $this->parseExprWithCapturedStmts($right); + + $code = '[&]() -> php::Var {' . PHP_EOL; + $code .= $this->formatCapturedStmtLines($keyBefore); + $code .= $this->getIndent() . $key . ' = ' . $keyExpr . ';' . PHP_EOL; + $code .= $this->formatCapturedStmtLines($keyAfter); + $code .= $this->formatCapturedStmtLines($rightBefore); + $code .= $this->getIndent() . $result . ' = ' . $rightExpr . ';' . PHP_EOL; + $code .= $this->formatCapturedStmtLines($rightAfter); + $code .= $this->getIndent() . $store . ';' . PHP_EOL; + $code .= $this->getIndent() . 'return ' . $result . ';' . PHP_EOL; + return $code . $this->getIndent() . '}()'; + } $rightClass = $this->detectClassOfExpr($right); // A Native-element std container owns a PHPX Box but its raw pointer @@ -703,6 +739,7 @@ trait AssignOpTrait protected function parseAssignOp(Expr\AssignOp $node, string $op): string { + $this->assertNativeArrayAccessDirectWrite($node->var, false); $this->assertNativeObjectOperatorOperandSupported($node->var, $node, $op); $this->assertNotNullsafeWriteContext($node->var); $this->assertNativePropertyHookDirectWriteTarget($node->var); @@ -1035,6 +1072,8 @@ trait AssignOpTrait protected function parseAssignRef(Expr\AssignRef $expr): string { + $this->assertNativeArrayAccessReferenceForbidden($expr->var); + $this->assertNativeArrayAccessReferenceForbidden($expr->expr); $this->assertNotNullsafeWriteContext($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->var); $this->assertNativePropertyHookDirectWriteTarget($expr->expr); @@ -1179,6 +1218,7 @@ trait AssignOpTrait protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string { + $this->assertNativeArrayAccessDirectWrite($expr->var, false); $this->checkLeftValue($expr->var); $rightClass = $this->detectClassOfExpr($expr->expr); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 8fe58b18..7e4b2d3f 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -788,11 +788,16 @@ trait PropertyAccessTrait $this->assertNotNullsafeWriteContext($var); $this->assertNativePropertyHookDirectWriteTarget($var); if ($this->isArrayDimFetch($var)) { - $this->assertNotNativeObjectArrayDimensionReceiver($var->var, $var); if ($var->dim === null) { $this->fatalError($var, 'Cannot use [] for array unset'); } - if ($this->isStdContainerExpr($var)) { + if ($this->isNativeObjectClass($this->detectClassOfExpr($var->var))) { + $lines[] = $this->parseNativeArrayAccessCall( + $var, + 'offsetUnset', + [new Node\Arg($var->dim)], + ) . ';'; + } elseif ($this->isStdContainerExpr($var)) { $lines[] = $this->parseStdContainerOffsetUnset($var) . ';'; } else { $array = $this->parseIdentifier($var->var); diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index 8a4c0fbb..d8841f2f 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -256,7 +256,11 @@ trait SelectionExpressionTrait if ($this->isNativeObjectClass($nativeClass)) { return $this->parseNativeValueSelection($left, $right, $nativeClass); } - $leftExpr = $this->parseIdentifier($left); + $nativeArrayAccessLeft = $left instanceof Expr\ArrayDimFetch + && $this->isNativeObjectClass($this->detectClassOfExpr($left->var)); + // Native ArrayAccess coalesce must let the presence helper evaluate + // offsetExists() before offsetGet(), with receiver/key evaluated once. + $leftExpr = $nativeArrayAccessLeft ? '' : $this->parseIdentifier($left); if ($this->isVarExpr($left)) { $this->checkVarMustExist($left, $leftExpr); } diff --git a/tests/compiler/native-class/array-access.phpt b/tests/compiler/native-class/array-access.phpt new file mode 100644 index 00000000..4bc7d809 --- /dev/null +++ b/tests/compiler/native-class/array-access.phpt @@ -0,0 +1,107 @@ +--TEST-- +Native class: ArrayAccess syntax lowers to direct native method calls +--FILE-- +values[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->values[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($offset === null) { + $this->values[] = $value; + } else { + $this->values[$offset] = $value; + } + } + + public function offsetUnset(mixed $offset): void + { + unset($this->values[$offset]); + } +} + +function receiver(NativeArrayBag $bag): NativeArrayBag +{ + echo 'R'; + return $bag; +} + +function keyValue(): string +{ + echo 'K'; + return 'ordered'; +} + +function assignedValue(): int +{ + echo 'V'; + return 42; +} + +function countedKey(string $key): string +{ + echo 'Q'; + return $key; +} + +function main(): void +{ + $bag = new NativeArrayBag(); + + $bag['first'] = 1; + $assigned = ($bag['second'] = 2); + $bag[] = 3; + $bag['nested'] = ['child' => 7]; + + var_dump($bag['first']); + var_dump($assigned); + var_dump($bag[0]); + var_dump($bag['nested']['child']); + var_dump(isset($bag['nested']['child'])); + var_dump($bag['nested']['child'] ?? 'fallback'); + var_dump(isset($bag['second'])); + var_dump(empty($bag['missing'])); + var_dump($bag['missing'] ?? 'fallback'); + var_dump(isset($bag[countedKey('second')])); + var_dump(empty($bag[countedKey('missing')])); + var_dump($bag[countedKey('missing')] ?? 'fallback'); + + unset($bag['second']); + var_dump(isset($bag['second'])); + + receiver($bag)[keyValue()] = assignedValue(); + echo PHP_EOL; + $ordered = ($bag['result'] = 42); + var_dump($ordered, $bag['ordered']); +} +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(7) +bool(true) +int(7) +bool(true) +bool(true) +string(8) "fallback" +Qbool(true) +Qbool(true) +Qstring(8) "fallback" +bool(false) +RKV +int(42) +int(42)