- Add support for array references using byRef flag in ArrayExpressionTrait - Modify hasReference check in array parsing logic to handle mixed references - Implement reference value parsing with proper error handling for unpacking - Enhance foreach reference binding with improved type checking and error messages - Add proper scope handling for foreach iterators with class context - Update foreach value assignment to use correct reference expression syntax - Implement structural mutation protection for std containers during iteration - Add comprehensive test coverage for foreach edge cases and reference mutations - Document std container modification restrictions during foreach loops - Update yield from and foreach documentation for iterator handling differencespull/18/head
parent
10d807bed6
commit
1403590a32
14 changed files with 508 additions and 26 deletions
@ -0,0 +1,75 @@ |
|||||||
|
--TEST-- |
||||||
|
foreach handles sparse mixed arrays, evaluates sources once, and releases cursors on control flow |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class ForeachArraySource |
||||||
|
{ |
||||||
|
public static int $calls = 0; |
||||||
|
|
||||||
|
public static function values(): array |
||||||
|
{ |
||||||
|
++self::$calls; |
||||||
|
return [0 => 'zero', 3 => 'three', 'name' => 'value']; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function stopEarly(array $values): string |
||||||
|
{ |
||||||
|
foreach ($values as $value) { |
||||||
|
return $value; |
||||||
|
} |
||||||
|
return 'empty'; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$seen = []; |
||||||
|
foreach (ForeachArraySource::values() as $key => $value) { |
||||||
|
if ($key === 3) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$seen[$key] = $value; |
||||||
|
} |
||||||
|
var_dump(ForeachArraySource::$calls, $seen); |
||||||
|
|
||||||
|
$empty = []; |
||||||
|
foreach ($empty as $value) { |
||||||
|
echo "unreachable\n"; |
||||||
|
} |
||||||
|
|
||||||
|
$inner = 1; |
||||||
|
$references = [&$inner]; |
||||||
|
foreach ($references as $value) { |
||||||
|
$value = 9; |
||||||
|
} |
||||||
|
var_dump($inner); |
||||||
|
|
||||||
|
foreach ($references as &$value) { |
||||||
|
$value = 11; |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
var_dump($inner); |
||||||
|
var_dump(stopEarly(['first', 'second'])); |
||||||
|
|
||||||
|
try { |
||||||
|
foreach (new ArrayIterator([1, 2]) as $value) { |
||||||
|
throw new RuntimeException('stop'); |
||||||
|
} |
||||||
|
} catch (RuntimeException $exception) { |
||||||
|
echo $exception->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(4) "zero" |
||||||
|
["name"]=> |
||||||
|
string(5) "value" |
||||||
|
} |
||||||
|
int(1) |
||||||
|
int(11) |
||||||
|
string(5) "first" |
||||||
|
stop |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
--TEST-- |
||||||
|
foreach invokes only required Iterator callbacks and cleans up exceptional cursors |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class CallbackIterator implements Iterator |
||||||
|
{ |
||||||
|
public static int $keyCalls = 0; |
||||||
|
private int $position = 0; |
||||||
|
|
||||||
|
public function __construct(private string $failure = '') |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function rewind(): void |
||||||
|
{ |
||||||
|
if ($this->failure === 'rewind') throw new RuntimeException('rewind'); |
||||||
|
$this->position = 0; |
||||||
|
} |
||||||
|
|
||||||
|
public function valid(): bool |
||||||
|
{ |
||||||
|
if ($this->failure === 'valid') throw new RuntimeException('valid'); |
||||||
|
return $this->position < 2; |
||||||
|
} |
||||||
|
|
||||||
|
public function current(): mixed |
||||||
|
{ |
||||||
|
if ($this->failure === 'current') throw new RuntimeException('current'); |
||||||
|
return $this->position + 10; |
||||||
|
} |
||||||
|
|
||||||
|
public function key(): mixed |
||||||
|
{ |
||||||
|
++self::$keyCalls; |
||||||
|
if ($this->failure === 'key') throw new RuntimeException('key'); |
||||||
|
return $this->position; |
||||||
|
} |
||||||
|
|
||||||
|
public function next(): void |
||||||
|
{ |
||||||
|
if ($this->failure === 'next') throw new RuntimeException('next'); |
||||||
|
++$this->position; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class LifetimeIterator extends CallbackIterator |
||||||
|
{ |
||||||
|
public static int $destroyed = 0; |
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
++self::$destroyed; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class LifetimeAggregate implements IteratorAggregate |
||||||
|
{ |
||||||
|
public function getIterator(): Traversable |
||||||
|
{ |
||||||
|
return new LifetimeIterator(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function consume(string $failure, bool $withKey): void |
||||||
|
{ |
||||||
|
try { |
||||||
|
if ($withKey) { |
||||||
|
foreach (new CallbackIterator($failure) as $key => $value) { |
||||||
|
} |
||||||
|
} else { |
||||||
|
foreach (new CallbackIterator($failure) as $value) { |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (RuntimeException $exception) { |
||||||
|
echo $exception->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
foreach (new CallbackIterator() as $value) { |
||||||
|
echo $value, "\n"; |
||||||
|
} |
||||||
|
var_dump(CallbackIterator::$keyCalls); |
||||||
|
|
||||||
|
foreach (new CallbackIterator() as $key => $value) { |
||||||
|
} |
||||||
|
var_dump(CallbackIterator::$keyCalls); |
||||||
|
|
||||||
|
consume('rewind', false); |
||||||
|
consume('valid', false); |
||||||
|
consume('current', false); |
||||||
|
consume('next', false); |
||||||
|
consume('key', true); |
||||||
|
|
||||||
|
foreach (new LifetimeAggregate() as $value) { |
||||||
|
break; |
||||||
|
} |
||||||
|
var_dump(LifetimeIterator::$destroyed); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
10 |
||||||
|
11 |
||||||
|
int(0) |
||||||
|
int(2) |
||||||
|
rewind |
||||||
|
valid |
||||||
|
current |
||||||
|
next |
||||||
|
key |
||||||
|
int(1) |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
--TEST-- |
||||||
|
foreach plain objects preserves scope, live properties, references, and typed-property rules |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ForeachScopeParent |
||||||
|
{ |
||||||
|
public int $public = 1; |
||||||
|
protected int $protected = 2; |
||||||
|
private int $parentPrivate = 3; |
||||||
|
} |
||||||
|
|
||||||
|
class ForeachScopeChild extends ForeachScopeParent |
||||||
|
{ |
||||||
|
private int $childPrivate = 4; |
||||||
|
public int $typed = 5; |
||||||
|
public readonly int $readonly; |
||||||
|
public string $uninitialized; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->readonly = 6; |
||||||
|
} |
||||||
|
|
||||||
|
public function visibleProperties(): array |
||||||
|
{ |
||||||
|
$seen = []; |
||||||
|
foreach ($this as $key => $value) { |
||||||
|
$seen[$key] = $value; |
||||||
|
} |
||||||
|
return $seen; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class ForeachTypedOnly |
||||||
|
{ |
||||||
|
public int $number = 7; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = (object) ['a' => 1, 'b' => 2]; |
||||||
|
$seen = []; |
||||||
|
foreach ($object as $key => $value) { |
||||||
|
$seen[$key] = $value; |
||||||
|
if ($key === 'a') { |
||||||
|
unset($object->b); |
||||||
|
$object->c = 3; |
||||||
|
} |
||||||
|
} |
||||||
|
var_dump($seen, $object->a, $object->c); |
||||||
|
|
||||||
|
foreach ($object as &$value) { |
||||||
|
$value *= 10; |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
var_dump($object->a, $object->c); |
||||||
|
|
||||||
|
$scoped = new ForeachScopeChild(); |
||||||
|
var_dump($scoped->visibleProperties()); |
||||||
|
|
||||||
|
try { |
||||||
|
foreach ($scoped as &$value) { |
||||||
|
} |
||||||
|
} catch (Error $error) { |
||||||
|
var_dump(str_contains($error->getMessage(), 'readonly property')); |
||||||
|
} |
||||||
|
|
||||||
|
$typed = new ForeachTypedOnly(); |
||||||
|
try { |
||||||
|
foreach ($typed as &$value) { |
||||||
|
$value = 'invalid'; |
||||||
|
} |
||||||
|
} catch (TypeError $error) { |
||||||
|
var_dump(str_contains($error->getMessage(), 'int')); |
||||||
|
} |
||||||
|
var_dump($typed->number); |
||||||
|
|
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
["a"]=> |
||||||
|
int(1) |
||||||
|
["c"]=> |
||||||
|
int(3) |
||||||
|
} |
||||||
|
int(1) |
||||||
|
int(3) |
||||||
|
int(10) |
||||||
|
int(30) |
||||||
|
array(6) { |
||||||
|
["public"]=> |
||||||
|
int(1) |
||||||
|
["protected"]=> |
||||||
|
int(2) |
||||||
|
["childPrivate"]=> |
||||||
|
int(4) |
||||||
|
["typed"]=> |
||||||
|
int(5) |
||||||
|
["readonly"]=> |
||||||
|
int(6) |
||||||
|
["uninitialized"]=> |
||||||
|
string(0) "" |
||||||
|
} |
||||||
|
bool(true) |
||||||
|
bool(true) |
||||||
|
int(7) |
||||||
@ -0,0 +1,100 @@ |
|||||||
|
--TEST-- |
||||||
|
foreach reference preserves COW, live mutations, and loop-variable aliases |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$values = [1, 2]; |
||||||
|
$copy = $values; |
||||||
|
foreach ($values as &$value) { |
||||||
|
$value *= 10; |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
var_dump($values, $copy); |
||||||
|
|
||||||
|
$values = [1, 2]; |
||||||
|
$seen = []; |
||||||
|
foreach ($values as &$value) { |
||||||
|
$seen[] = $value; |
||||||
|
if ($value === 1) { |
||||||
|
$values[] = 3; |
||||||
|
} |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
var_dump($seen); |
||||||
|
|
||||||
|
$values = [1, 2, 3]; |
||||||
|
$seen = []; |
||||||
|
foreach ($values as $key => &$value) { |
||||||
|
$seen[] = [$key, $value]; |
||||||
|
if ($key === 0) { |
||||||
|
unset($values[1]); |
||||||
|
} |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
var_dump($seen); |
||||||
|
|
||||||
|
$value = 99; |
||||||
|
foreach ($values as &$value) { |
||||||
|
++$value; |
||||||
|
} |
||||||
|
unset($value); |
||||||
|
foreach ([7, 8] as $value) { |
||||||
|
echo $value, "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
$linked = [1, 2]; |
||||||
|
foreach ($linked as &$slot) { |
||||||
|
} |
||||||
|
foreach ([4, 5] as $slot) { |
||||||
|
} |
||||||
|
unset($slot); |
||||||
|
var_dump($linked); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(10) |
||||||
|
[1]=> |
||||||
|
int(20) |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
|
array(3) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(2) |
||||||
|
[2]=> |
||||||
|
int(3) |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(0) |
||||||
|
[1]=> |
||||||
|
int(1) |
||||||
|
} |
||||||
|
[1]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(2) |
||||||
|
[1]=> |
||||||
|
int(3) |
||||||
|
} |
||||||
|
} |
||||||
|
7 |
||||||
|
8 |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(5) |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
--TEST-- |
||||||
|
std containers allow non-structural element updates during foreach |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$vector = std::vector(Type::Int); |
||||||
|
$vector[] = 1; |
||||||
|
$vector[] = 2; |
||||||
|
foreach ($vector as $vectorKey => $vectorValue) { |
||||||
|
$vector[$vectorKey] += 10; |
||||||
|
} |
||||||
|
var_dump($vector[0], $vector[1]); |
||||||
|
|
||||||
|
$map = std::ordered_map(Type::String, Type::Int); |
||||||
|
$map['a'] = 3; |
||||||
|
$map['b'] = 4; |
||||||
|
foreach ($map as $mapKey => $mapValue) { |
||||||
|
$map[$mapKey] += 20; |
||||||
|
} |
||||||
|
var_dump($map['a'], $map['b']); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(11) |
||||||
|
int(12) |
||||||
|
int(23) |
||||||
|
int(24) |
||||||
Loading…
Reference in new issue