Skip to content

Commit 0c65c0c

Browse files
authored
Preserving order in Map and Set using doubly linked lists (#706)
* Added order tests * Added key ordering using doubly linked lists for Map and Set * Changed Set constructor to use this.add * Fixed some issues and added tests to catch them * Use luatable instead of hacky objects for Set and Map internal storage * map.has() correctly deal with undefined values * Changed map and set luatype property definitions * Updated lualib LuaTable declaration * Grouped iteration order tests in describe * Used LuaTable for WeakMap and WeakSet
1 parent 1ab86da commit 0c65c0c

File tree

7 files changed

+307
-65
lines changed

7 files changed

+307
-65
lines changed

src/lualib/Map.ts

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ Map = class Map<K, V> {
22
public static [Symbol.species] = Map;
33
public [Symbol.toStringTag] = "Map";
44

5-
// Type of key is actually K
6-
private items: { [key: string]: V } = {};
5+
private items = new LuaTable<K, V>();
76
public size = 0;
87

8+
// Key-order variables
9+
private firstKey: K | undefined;
10+
private lastKey: K | undefined;
11+
private nextKey = new LuaTable<K, K>();
12+
private previousKey = new LuaTable<K, K>();
13+
914
constructor(entries?: Iterable<readonly [K, V]> | Array<readonly [K, V]>) {
1015
if (entries === undefined) return;
1116

@@ -23,15 +28,18 @@ Map = class Map<K, V> {
2328
}
2429
} else {
2530
const array = entries as Array<[K, V]>;
26-
this.size = array.length;
2731
for (const kvp of array) {
28-
this.items[kvp[0] as any] = kvp[1];
32+
this.set(kvp[0], kvp[1]);
2933
}
3034
}
3135
}
3236

3337
public clear(): void {
34-
this.items = {};
38+
this.items = new LuaTable();
39+
this.nextKey = new LuaTable();
40+
this.previousKey = new LuaTable();
41+
this.firstKey = undefined;
42+
this.lastKey = undefined;
3543
this.size = 0;
3644
return;
3745
}
@@ -40,31 +48,64 @@ Map = class Map<K, V> {
4048
const contains = this.has(key);
4149
if (contains) {
4250
this.size--;
51+
52+
// Do order bookkeeping
53+
const next = this.nextKey.get(key);
54+
const previous = this.previousKey.get(key);
55+
if (next && previous) {
56+
this.nextKey.set(previous, next);
57+
this.previousKey.set(next, previous);
58+
} else if (next) {
59+
this.firstKey = next;
60+
this.previousKey.set(next, undefined);
61+
} else if (previous) {
62+
this.lastKey = previous;
63+
this.nextKey.set(previous, undefined);
64+
} else {
65+
this.firstKey = undefined;
66+
this.lastKey = undefined;
67+
}
68+
69+
this.nextKey.set(key, undefined);
70+
this.previousKey.set(key, undefined);
4371
}
44-
this.items[key as any] = undefined;
72+
this.items.set(key, undefined);
73+
4574
return contains;
4675
}
4776

4877
public forEach(callback: (value: V, key: K, map: Map<K, V>) => any): void {
49-
for (const key in this.items) {
50-
callback(this.items[key], key as any, this);
78+
for (const key of this.keys()) {
79+
callback(this.items.get(key), key, this);
5180
}
5281
return;
5382
}
5483

5584
public get(key: K): V | undefined {
56-
return this.items[key as any];
85+
return this.items.get(key);
5786
}
5887

5988
public has(key: K): boolean {
60-
return this.items[key as any] !== undefined;
89+
return this.nextKey.get(key) !== undefined || this.lastKey === key;
6190
}
6291

6392
public set(key: K, value: V): this {
64-
if (!this.has(key)) {
93+
const isNewValue = !this.has(key);
94+
if (isNewValue) {
6595
this.size++;
6696
}
67-
this.items[key as any] = value;
97+
this.items.set(key, value);
98+
99+
// Do order bookkeeping
100+
if (this.firstKey === undefined) {
101+
this.firstKey = key;
102+
this.lastKey = key;
103+
} else if (isNewValue) {
104+
this.nextKey.set(this.lastKey, key);
105+
this.previousKey.set(key, this.lastKey);
106+
this.lastKey = key;
107+
}
108+
68109
return this;
69110
}
70111

@@ -74,44 +115,47 @@ Map = class Map<K, V> {
74115

75116
public entries(): IterableIterator<[K, V]> {
76117
const items = this.items;
77-
let key: K;
78-
let value: V;
118+
const nextKey = this.nextKey;
119+
let key = this.firstKey;
79120
return {
80121
[Symbol.iterator](): IterableIterator<[K, V]> {
81122
return this;
82123
},
83124
next(): IteratorResult<[K, V]> {
84-
[key, value] = next(items, key);
85-
return { done: !key, value: [key, value] };
125+
const result = { done: !key, value: [key, items.get(key)] as [K, V] };
126+
key = nextKey.get(key);
127+
return result;
86128
},
87129
};
88130
}
89131

90132
public keys(): IterableIterator<K> {
91-
const items = this.items;
92-
let key: K;
133+
const nextKey = this.nextKey;
134+
let key = this.firstKey;
93135
return {
94136
[Symbol.iterator](): IterableIterator<K> {
95137
return this;
96138
},
97139
next(): IteratorResult<K> {
98-
[key] = next(items, key);
99-
return { done: !key, value: key };
140+
const result = { done: !key, value: key };
141+
key = nextKey.get(key);
142+
return result;
100143
},
101144
};
102145
}
103146

104147
public values(): IterableIterator<V> {
105148
const items = this.items;
106-
let key: K;
107-
let value: V;
149+
const nextKey = this.nextKey;
150+
let key = this.firstKey;
108151
return {
109152
[Symbol.iterator](): IterableIterator<V> {
110153
return this;
111154
},
112155
next(): IteratorResult<V> {
113-
[key, value] = next(items, key);
114-
return { done: !key, value };
156+
const result = { done: !key, value: items.get(key) };
157+
key = nextKey.get(key);
158+
return result;
115159
},
116160
};
117161
}

src/lualib/Set.ts

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ Set = class Set<T> {
22
public static [Symbol.species] = Set;
33
public [Symbol.toStringTag] = "Set";
44

5-
// Key type is actually T
6-
private items: { [key: string]: boolean } = {};
75
public size = 0;
86

7+
private firstKey: T | undefined;
8+
private lastKey: T | undefined;
9+
private nextKey = new LuaTable<T, T>();
10+
private previousKey = new LuaTable<T, T>();
11+
912
constructor(values?: Iterable<T> | T[]) {
1013
if (values === undefined) return;
1114

@@ -22,23 +25,36 @@ Set = class Set<T> {
2225
}
2326
} else {
2427
const array = values as T[];
25-
this.size = array.length;
2628
for (const value of array) {
27-
this.items[value as any] = true;
29+
this.add(value);
2830
}
2931
}
3032
}
3133

3234
public add(value: T): Set<T> {
33-
if (!this.has(value)) {
35+
const isNewValue = !this.has(value);
36+
if (isNewValue) {
3437
this.size++;
3538
}
36-
this.items[value as any] = true;
39+
40+
// Do order bookkeeping
41+
if (this.firstKey === undefined) {
42+
this.firstKey = value;
43+
this.lastKey = value;
44+
} else if (isNewValue) {
45+
this.nextKey.set(this.lastKey, value);
46+
this.previousKey.set(value, this.lastKey);
47+
this.lastKey = value;
48+
}
49+
3750
return this;
3851
}
3952

4053
public clear(): void {
41-
this.items = {};
54+
this.nextKey = new LuaTable();
55+
this.previousKey = new LuaTable();
56+
this.firstKey = undefined;
57+
this.lastKey = undefined;
4258
this.size = 0;
4359
return;
4460
}
@@ -47,63 +63,86 @@ Set = class Set<T> {
4763
const contains = this.has(value);
4864
if (contains) {
4965
this.size--;
66+
67+
// Do order bookkeeping
68+
const next = this.nextKey.get(value);
69+
const previous = this.previousKey.get(value);
70+
if (next && previous) {
71+
this.nextKey.set(previous, next);
72+
this.previousKey.set(next, previous);
73+
} else if (next) {
74+
this.firstKey = next;
75+
this.previousKey.set(next, undefined);
76+
} else if (previous) {
77+
this.lastKey = previous;
78+
this.nextKey.set(previous, undefined);
79+
} else {
80+
this.firstKey = undefined;
81+
this.lastKey = undefined;
82+
}
83+
84+
this.nextKey.set(value, undefined);
85+
this.previousKey.set(value, undefined);
5086
}
51-
this.items[value as any] = undefined;
87+
5288
return contains;
5389
}
5490

5591
public forEach(callback: (value: T, key: T, set: Set<T>) => any): void {
56-
for (const key in this.items) {
57-
callback(key as any, key as any, this);
92+
for (const key of this.keys()) {
93+
callback(key, key, this);
5894
}
5995
}
6096

6197
public has(value: T): boolean {
62-
return this.items[value as any] === true;
98+
return this.nextKey.get(value) !== undefined || this.lastKey === value;
6399
}
64100

65101
public [Symbol.iterator](): IterableIterator<T> {
66102
return this.values();
67103
}
68104

69105
public entries(): IterableIterator<[T, T]> {
70-
const items = this.items;
71-
let key: T;
106+
const nextKey = this.nextKey;
107+
let key: T = this.firstKey;
72108
return {
73109
[Symbol.iterator](): IterableIterator<[T, T]> {
74110
return this;
75111
},
76112
next(): IteratorResult<[T, T]> {
77-
[key] = next(items, key);
78-
return { done: !key, value: [key, key] };
113+
const result = { done: !key, value: [key, key] as [T, T] };
114+
key = nextKey.get(key);
115+
return result;
79116
},
80117
};
81118
}
82119

83120
public keys(): IterableIterator<T> {
84-
const items = this.items;
85-
let key: T;
121+
const nextKey = this.nextKey;
122+
let key: T = this.firstKey;
86123
return {
87124
[Symbol.iterator](): IterableIterator<T> {
88125
return this;
89126
},
90127
next(): IteratorResult<T> {
91-
[key] = next(items, key);
92-
return { done: !key, value: key };
128+
const result = { done: !key, value: key };
129+
key = nextKey.get(key);
130+
return result;
93131
},
94132
};
95133
}
96134

97135
public values(): IterableIterator<T> {
98-
const items = this.items;
99-
let key: T;
136+
const nextKey = this.nextKey;
137+
let key: T = this.firstKey;
100138
return {
101139
[Symbol.iterator](): IterableIterator<T> {
102140
return this;
103141
},
104142
next(): IteratorResult<T> {
105-
[key] = next(items, key);
106-
return { done: !key, value: key };
143+
const result = { done: !key, value: key };
144+
key = nextKey.get(key);
145+
return result;
107146
},
108147
};
109148
}

src/lualib/WeakMap.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ WeakMap = class WeakMap<K extends object, V> {
22
public static [Symbol.species] = WeakMap;
33
public [Symbol.toStringTag] = "WeakMap";
44

5-
// Type of key is actually K
6-
private items: { [key: string]: V } = {};
5+
private items = new LuaTable<K, V>();
76

87
constructor(entries?: Iterable<readonly [K, V]> | Array<readonly [K, V]>) {
98
setmetatable(this.items, { __mode: "k" });
@@ -19,31 +18,31 @@ WeakMap = class WeakMap<K extends object, V> {
1918
break;
2019
}
2120
const value: [K, V] = result.value; // Ensures index is offset when tuple is accessed
22-
this.set(value[0], value[1]);
21+
this.items.set(value[0], value[1]);
2322
}
2423
} else {
2524
for (const kvp of entries as Array<[K, V]>) {
26-
this.items[kvp[0] as any] = kvp[1];
25+
this.items.set(kvp[0], kvp[1]);
2726
}
2827
}
2928
}
3029

3130
public delete(key: K): boolean {
3231
const contains = this.has(key);
33-
this.items[key as any] = undefined;
32+
this.items.set(key, undefined);
3433
return contains;
3534
}
3635

3736
public get(key: K): V | undefined {
38-
return this.items[key as any];
37+
return this.items.get(key);
3938
}
4039

4140
public has(key: K): boolean {
42-
return this.items[key as any] !== undefined;
41+
return this.items.get(key) !== undefined;
4342
}
4443

4544
public set(key: K, value: V): this {
46-
this.items[key as any] = value;
45+
this.items.set(key, value);
4746
return this;
4847
}
4948
};

0 commit comments

Comments
 (0)