Skip to content

Commit 942cc3d

Browse files
authored
ListTag: use array instead of SplDoublyLinkedList (#128)
the idea of using linked-list for this was to allow dynamic insertions and deletions midway through the list. however, this is basically never done in practice, and because SplDoublyLinkedList has O(n) read performance, we pay a read performance penalty for these write features which are basically never used anyway. in the future, we should perhaps consider getting rid of the mutation APIs from ListTag, and require creating a new one to replace it in mutation cases, similar to other tags (aside from TAG_Compound).
1 parent 36c1dbf commit 942cc3d

File tree

3 files changed

+82
-48
lines changed

3 files changed

+82
-48
lines changed

src/tag/ListTag.php

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@
2828
use pocketmine\nbt\NbtStreamReader;
2929
use pocketmine\nbt\NbtStreamWriter;
3030
use pocketmine\nbt\ReaderTracker;
31-
use function assert;
31+
use function array_key_last;
32+
use function array_map;
33+
use function array_pop;
34+
use function array_push;
35+
use function array_shift;
36+
use function array_slice;
37+
use function array_unshift;
38+
use function array_values;
3239
use function count;
3340
use function func_num_args;
3441
use function get_class;
35-
use function iterator_to_array;
3642
use function str_repeat;
3743

3844
/**
@@ -44,20 +50,19 @@ final class ListTag extends Tag implements \Countable, \IteratorAggregate{
4450
/** @var int */
4551
private $tagType;
4652
/**
47-
* @var \SplDoublyLinkedList|Tag[]
48-
* @phpstan-var \SplDoublyLinkedList<Tag>
53+
* @var Tag[]
54+
* @phpstan-var list<Tag>
4955
*/
50-
private $value;
56+
private $value = [];
5157

5258
/**
5359
* @param Tag[] $value
5460
*/
5561
public function __construct(array $value = [], int $tagType = NBT::TAG_End){
5662
self::restrictArgCount(__METHOD__, func_num_args(), 2);
5763
$this->tagType = $tagType;
58-
$this->value = new \SplDoublyLinkedList();
5964
foreach($value as $tag){
60-
$this->push($tag);
65+
$this->push($tag); //ensure types get checked
6166
}
6267
}
6368

@@ -66,12 +71,7 @@ public function __construct(array $value = [], int $tagType = NBT::TAG_End){
6671
* @phpstan-return list<Tag>
6772
*/
6873
public function getValue() : array{
69-
$value = [];
70-
foreach($this->value as $v){
71-
$value[] = $v;
72-
}
73-
74-
return $value;
74+
return $this->value;
7575
}
7676

7777
/**
@@ -80,50 +80,51 @@ public function getValue() : array{
8080
* @phpstan-return list<mixed>
8181
*/
8282
public function getAllValues() : array{
83-
$result = [];
84-
foreach($this->value as $tag){
85-
$result[] = $tag->getValue();
86-
}
87-
88-
return $result;
83+
return array_map(fn(Tag $t) => $t->getValue(), $this->value);
8984
}
9085

9186
public function count() : int{
92-
return $this->value->count();
87+
return count($this->value);
9388
}
9489

9590
public function getCount() : int{
96-
return $this->value->count();
91+
return count($this->value);
9792
}
9893

9994
/**
10095
* Appends the specified tag to the end of the list.
10196
*/
10297
public function push(Tag $tag) : void{
10398
$this->checkTagType($tag);
104-
$this->value->push($tag);
99+
$this->value[] = $tag;
105100
}
106101

107102
/**
108103
* Removes the last tag from the list and returns it.
109104
*/
110105
public function pop() : Tag{
111-
return $this->value->pop();
106+
if(count($this->value) === 0){
107+
throw new \LogicException("List is empty");
108+
}
109+
return array_pop($this->value);
112110
}
113111

114112
/**
115113
* Adds the specified tag to the start of the list.
116114
*/
117115
public function unshift(Tag $tag) : void{
118116
$this->checkTagType($tag);
119-
$this->value->unshift($tag);
117+
array_unshift($this->value, $tag);
120118
}
121119

122120
/**
123121
* Removes the first tag from the list and returns it.
124122
*/
125123
public function shift() : Tag{
126-
return $this->value->shift();
124+
if(count($this->value) === 0){
125+
throw new \LogicException("List is empty");
126+
}
127+
return array_shift($this->value);
127128
}
128129

129130
/**
@@ -135,14 +136,23 @@ public function shift() : Tag{
135136
*/
136137
public function insert(int $offset, Tag $tag){
137138
$this->checkTagType($tag);
138-
$this->value->add($offset, $tag);
139+
if($offset < 0 || $offset > count($this->value)){
140+
throw new \OutOfRangeException("Offset cannot be negative or larger than the list's current size");
141+
}
142+
$newValue = array_slice($this->value, 0, $offset);
143+
$newValue[] = $tag;
144+
array_push($newValue, ...array_slice($this->value, $offset));
145+
$this->value = $newValue;
139146
}
140147

141148
/**
142149
* Removes a value from the list. All later tags in the list are moved down by 1 position.
143150
*/
144151
public function remove(int $offset) : void{
145-
unset($this->value[$offset]);
152+
//to keep phpstan happy we can't directly unset from $this->value
153+
$newValue = $this->value;
154+
unset($newValue[$offset]);
155+
$this->value = array_values($newValue);
146156
}
147157

148158
/**
@@ -161,14 +171,20 @@ public function get(int $offset) : Tag{
161171
* Returns the element in the first position of the list, without removing it.
162172
*/
163173
public function first() : Tag{
164-
return $this->value->bottom();
174+
if(count($this->value) === 0){
175+
throw new \LogicException("List is empty");
176+
}
177+
return $this->value[0];
165178
}
166179

167180
/**
168181
* Returns the element in the last position in the list (the end), without removing it.
169182
*/
170183
public function last() : Tag{
171-
return $this->value->top();
184+
if(count($this->value) === 0){
185+
throw new \LogicException("List is empty");
186+
}
187+
return $this->value[array_key_last($this->value)];
172188
}
173189

174190
/**
@@ -178,6 +194,9 @@ public function last() : Tag{
178194
*/
179195
public function set(int $offset, Tag $tag) : void{
180196
$this->checkTagType($tag);
197+
if($offset < 0 || $offset > count($this->value)){ //allow setting the end offset
198+
throw new \OutOfRangeException("Offset cannot be negative or larger than the list's current size");
199+
}
181200
$this->value[$offset] = $tag;
182201
}
183202

@@ -192,7 +211,7 @@ public function isset(int $offset) : bool{
192211
* Returns whether there are any tags in the list.
193212
*/
194213
public function empty() : bool{
195-
return $this->value->isEmpty();
214+
return count($this->value) === 0;
196215
}
197216

198217
protected function getTypeName() : string{
@@ -218,7 +237,7 @@ public function getTagType() : int{
218237
* @throws \LogicException if the list is not empty
219238
*/
220239
public function setTagType(int $type){
221-
if(!$this->value->isEmpty()){
240+
if(count($this->value) > 0){
222241
throw new \LogicException("Cannot change tag type of non-empty ListTag");
223242
}
224243
$this->tagType = $type;
@@ -264,31 +283,22 @@ public static function read(NbtStreamReader $reader, ReaderTracker $tracker) : s
264283

265284
public function write(NbtStreamWriter $writer) : void{
266285
$writer->writeByte($this->tagType);
267-
$writer->writeInt($this->value->count());
268-
/** @var Tag $tag */
286+
$writer->writeInt(count($this->value));
269287
foreach($this->value as $tag){
270288
$tag->write($writer);
271289
}
272290
}
273291

274292
protected function stringifyValue(int $indentation) : string{
275293
$str = "{\n";
276-
/** @var Tag $tag */
277294
foreach($this->value as $tag){
278295
$str .= str_repeat(" ", $indentation + 1) . $tag->toString($indentation + 1) . "\n";
279296
}
280297
return $str . str_repeat(" ", $indentation) . "}";
281298
}
282299

283300
public function __clone(){
284-
/** @phpstan-var \SplDoublyLinkedList<Tag> $new */
285-
$new = new \SplDoublyLinkedList();
286-
287-
foreach($this->value as $tag){
288-
$new->push($tag->safeClone());
289-
}
290-
291-
$this->value = $new;
301+
$this->value = array_map(fn(Tag $t) => $t->safeClone(), $this->value);
292302
}
293303

294304
protected function makeCopy(){
@@ -300,20 +310,16 @@ protected function makeCopy(){
300310
* @phpstan-return \Generator<int, Tag, void, void>
301311
*/
302312
public function getIterator() : \Generator{
303-
//we technically don't need iterator_to_array() here, but I don't feel comfortable relying on "yield from" to
304-
//copy the underlying dataset referenced by SplDoublyLinkedList
305-
yield from iterator_to_array($this->value, true);
313+
yield from $this->value;
306314
}
307315

308316
public function equals(Tag $that) : bool{
309317
if(!($that instanceof $this) or count($this->value) !== count($that->value)){
310318
return false;
311319
}
312320

313-
$thatValues = $that->getValue(); //SplDoublyLinkedList has O(n) random access, so this is faster than repeated get() calls
314321
foreach($this->value as $k => $v){
315-
assert(isset($thatValues[$k]), "We checked the count above, so this should not be missing");
316-
if(!$v->equals($thatValues[$k])){
322+
if(!$v->equals($that->value[$k])){
317323
return false;
318324
}
319325
}

tests/phpstan/configs/phpstan-bugs.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ parameters:
1111
identifier: arrayValues.list
1212
count: 1
1313
path: ../../../src/tag/IntArrayTag.php
14+
15+
-
16+
message: '#^Property pocketmine\\nbt\\tag\\ListTag\:\:\$value \(list\<pocketmine\\nbt\\tag\\Tag\>\) does not accept non\-empty\-array\<int\<0, max\>, pocketmine\\nbt\\tag\\Tag\>\.$#'
17+
identifier: assign.propertyType
18+
count: 1
19+
path: ../../../src/tag/ListTag.php

tests/phpunit/tag/ListTagTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use pocketmine\nbt\NBT;
2828
use function array_fill;
2929
use function array_key_first;
30+
use function array_keys;
3031
use function array_map;
3132

3233
class ListTagTest extends TestCase{
@@ -154,4 +155,25 @@ public function testModificationDuringIteration() : void{
154155
//if we iterated by-ref, entries are likely to have been skipped
155156
self::assertCount(0, $tag);
156157
}
158+
159+
public function testInsert() : void{
160+
$list = new ListTag();
161+
$list->push(new IntTag(0));
162+
163+
$list->insert(1, new IntTag(2));
164+
$list->insert(1, new IntTag(1)); //displaces int(2)
165+
166+
self::assertSame([0, 1, 2], $list->getAllValues());
167+
self::assertSame([0, 1, 2], array_keys($list->getValue()), "Key order should be consecutive");
168+
}
169+
170+
public function testDelete() : void{
171+
$list = new ListTag();
172+
foreach(range(0, 2) as $value){
173+
$list->push(new IntTag($value));
174+
}
175+
$list->remove(1);
176+
self::assertSame([0, 2], $list->getAllValues());
177+
self::assertSame([0, 1], array_keys($list->getValue()));
178+
}
157179
}

0 commit comments

Comments
 (0)