1+ // Insertion-ordered Map using flat arrays with tombstone deletion and
2+ // V8-style version chain compaction. Deleted entries become nil (tombstones);
3+ // iterators skip them via idx++. Compaction creates new arrays and links
4+ // old → new at index 0; iterators transition lazily by adjusting their
5+ // index (subtracting holes before current position).
6+ //
7+ // Using integer-indexed arrays (orderedKeys/orderedValues) for the insertion
8+ // order leverages Lua's array part: sequential integer keys are stored in
9+ // contiguous memory, making iteration a simple pointer offset (~2-3 cycles)
10+ // rather than a hash table lookup (~15-50 cycles per step).
11+ //
12+ // Based on V8's OrderedHashTable (ordered-hash-table.cc: Rehash, Transition):
13+ // https://chromium.googlesource.com/v8/v8/+/main/src/objects/ordered-hash-table.cc
114export class Map < K extends AnyNotNil , V > {
215 public static [ Symbol . species ] = Map ;
316 public [ Symbol . toStringTag ] = "Map" ;
417
5- private items = new LuaTable < K , V > ( ) ;
618 public size = 0 ;
719
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 > ( ) ;
20+ // Flat array storage (1-based indices for Lua)
21+ private keyIndex = new LuaTable < K , number > ( ) ;
22+ private orderedKeys = new LuaTable < number , K > ( ) ;
23+ private orderedValues = new LuaTable < number , V > ( ) ;
24+ private nextSlot = 1 ;
25+ private deletedCount = 0 ;
1326
1427 constructor ( entries ?: Iterable < readonly [ K , V ] > | Array < readonly [ K , V ] > ) {
1528 if ( entries === undefined ) return ;
@@ -36,141 +49,214 @@ export class Map<K extends AnyNotNil, V> {
3649 }
3750
3851 public clear ( ) : void {
39- this . items = new LuaTable ( ) ;
40- this . nextKey = new LuaTable ( ) ;
41- this . previousKey = new LuaTable ( ) ;
42- this . firstKey = undefined ;
43- this . lastKey = undefined ;
52+ this . keyIndex = new LuaTable ( ) ;
53+ this . orderedKeys = new LuaTable ( ) ;
54+ this . orderedValues = new LuaTable ( ) ;
55+ this . nextSlot = 1 ;
4456 this . size = 0 ;
57+ this . deletedCount = 0 ;
4558 }
4659
4760 public delete ( key : K ) : boolean {
48- const contains = this . has ( key ) ;
49- if ( contains ) {
50- this . size -- ;
51-
52- // Do order bookkeeping
53- const next = this . nextKey . get ( key ) ;
54- const previous = this . previousKey . get ( key ) ;
55- if ( next !== undefined && previous !== undefined ) {
56- this . nextKey . set ( previous , next ) ;
57- this . previousKey . set ( next , previous ) ;
58- } else if ( next !== undefined ) {
59- this . firstKey = next ;
60- this . previousKey . set ( next , undefined ! ) ;
61- } else if ( previous !== undefined ) {
62- this . lastKey = previous ;
63- this . nextKey . set ( previous , undefined ! ) ;
61+ const idx = this . keyIndex . get ( key ) ;
62+ if ( idx === undefined ) return false ;
63+ this . size -- ;
64+ this . deletedCount ++ ;
65+ this . keyIndex . delete ( key ) ;
66+ this . orderedKeys . delete ( idx ) ;
67+ this . orderedValues . delete ( idx ) ;
68+
69+ if ( this . deletedCount > this . size ) {
70+ this . compact ( ) ;
71+ }
72+ return true ;
73+ }
74+
75+ // Compaction is needed because nextSlot grows monotonically — deleted
76+ // entries leave nil tombstones but Lua tables don't shrink on nil
77+ // assignment. Lua only resizes a table during rehash, and rehash only
78+ // triggers when a hash-part insert exhausts free nodes (ltable.c:
79+ // luaH_newkey → getfreepos → rehash). Since orderedKeys uses sequential
80+ // integer indices (array part), normal inserts rarely touch the hash
81+ // part, so rehash won't trigger naturally to reclaim the space.
82+ //
83+ // V8-style: copy live entries to new arrays, record hole positions in
84+ // the old array at negative indices, link old[0] → new. Active iterators
85+ // hold old arrays and transition lazily on next() by adjusting their
86+ // index (subtracting holes before current position).
87+ //
88+ // Index 0 and negative indices go to Lua's hash part (not array part),
89+ // so they don't conflict with the 1-based data entries.
90+ private compact ( ) : void {
91+ const oldKeys = this . orderedKeys ;
92+ const oldValues = this . orderedValues ;
93+ const oldNextSlot = this . nextSlot ;
94+ const newKeys = new LuaTable < number , K > ( ) ;
95+ const newValues = new LuaTable < number , V > ( ) ;
96+ let newSlot = 1 ;
97+ let holeCount = 0 ;
98+
99+ for ( let i = 1 ; i < oldNextSlot ; i ++ ) {
100+ const k = oldKeys . get ( i ) ;
101+ if ( k !== undefined ) {
102+ newKeys . set ( newSlot , k ) ;
103+ newValues . set ( newSlot , oldValues . get ( i ) ) ;
104+ this . keyIndex . set ( k , newSlot ) ;
105+ newSlot ++ ;
64106 } else {
65- this . firstKey = undefined ;
66- this . lastKey = undefined ;
107+ holeCount ++ ;
108+ oldKeys . set ( - holeCount , i as any ) ;
67109 }
68-
69- this . nextKey . set ( key , undefined ! ) ;
70- this . previousKey . set ( key , undefined ! ) ;
71110 }
72- this . items . set ( key , undefined ! ) ;
73111
74- return contains ;
112+ oldKeys . set ( 0 , newKeys as any ) ;
113+ oldValues . set ( 0 , newValues as any ) ;
114+
115+ this . orderedKeys = newKeys ;
116+ this . orderedValues = newValues ;
117+ this . nextSlot = newSlot ;
118+ this . deletedCount = 0 ;
75119 }
76120
77121 public forEach ( callback : ( value : V , key : K , map : Map < K , V > ) => any ) : void {
78122 for ( const key of this . keys ( ) ) {
79- callback ( this . items . get ( key ) , key , this ) ;
123+ callback ( this . orderedValues . get ( this . keyIndex . get ( key ) ) , key , this ) ;
80124 }
81125 }
82126
83127 public get ( key : K ) : V | undefined {
84- return this . items . get ( key ) ;
128+ const idx = this . keyIndex . get ( key ) ;
129+ if ( idx === undefined ) return undefined ;
130+ return this . orderedValues . get ( idx ) ;
85131 }
86132
87133 public has ( key : K ) : boolean {
88- return this . nextKey . get ( key ) !== undefined || this . lastKey === key ;
134+ return this . keyIndex . get ( key ) !== undefined ;
89135 }
90136
91137 public set ( key : K , value : V ) : this {
92- const isNewValue = ! this . has ( key ) ;
93- if ( isNewValue ) {
138+ const existingIdx = this . keyIndex . get ( key ) ;
139+ if ( existingIdx !== undefined ) {
140+ this . orderedValues . set ( existingIdx , value ) ;
141+ } else {
94142 this . size ++ ;
143+ const idx = this . nextSlot ;
144+ this . nextSlot = idx + 1 ;
145+ this . keyIndex . set ( key , idx ) ;
146+ this . orderedKeys . set ( idx , key ) ;
147+ this . orderedValues . set ( idx , value ) ;
95148 }
96- this . items . set ( key , value ) ;
97-
98- // Do order bookkeeping
99- if ( this . firstKey === undefined ) {
100- this . firstKey = key ;
101- this . lastKey = key ;
102- } else if ( isNewValue ) {
103- this . nextKey . set ( this . lastKey ! , key ) ;
104- this . previousKey . set ( key , this . lastKey ! ) ;
105- this . lastKey = key ;
106- }
107-
108149 return this ;
109150 }
110151
111152 public [ Symbol . iterator ] ( ) : IterableIterator < [ K , V ] > {
112153 return this . entries ( ) ;
113154 }
114155
156+ // entries/keys/values are intentionally kept inline (not refactored into
157+ // a shared helper) to avoid per-step function call overhead on this hot path.
115158 public entries ( ) : IterableIterator < [ K , V ] > {
116- const getFirstKey = ( ) => this . firstKey ;
117- const { items , nextKey } = this ;
118- let key : K | undefined ;
119- let started = false ;
159+ let keys = this . orderedKeys ;
160+ let vals = this . orderedValues ;
161+ const map = this ; // eslint-disable-line @typescript-eslint/no-this-alias
162+ let idx = 1 ;
120163 return {
121164 [ Symbol . iterator ] ( ) : IterableIterator < [ K , V ] > {
122165 return this ;
123166 } ,
124167 next ( ) : IteratorResult < [ K , V ] > {
125- if ( ! started ) {
126- started = true ;
127- key = getFirstKey ( ) ;
128- } else {
129- key = nextKey . get ( key ! ) ;
168+ while ( keys . get ( 0 ) !== undefined ) {
169+ let adj = 0 ;
170+ let h = 1 ;
171+ while ( true ) {
172+ const holePos = keys . get ( - h ) as any as number | undefined ;
173+ if ( holePos === undefined || holePos >= idx ) break ;
174+ adj ++ ;
175+ h ++ ;
176+ }
177+ idx -= adj ;
178+ vals = vals . get ( 0 ) as any ;
179+ keys = keys . get ( 0 ) as any ;
180+ }
181+ while ( idx < map . nextSlot && keys . get ( idx ) === undefined ) {
182+ idx ++ ;
183+ }
184+ if ( idx >= map . nextSlot ) {
185+ return { done : true , value : undefined ! } ;
130186 }
131- return { done : ! key , value : [ key ! , items . get ( key ! ) ] as [ K , V ] } ;
187+ const i = idx ;
188+ idx ++ ;
189+ return { done : false , value : [ keys . get ( i ) , vals . get ( i ) ] as [ K , V ] } ;
132190 } ,
133191 } ;
134192 }
135193
136194 public keys ( ) : IterableIterator < K > {
137- const getFirstKey = ( ) => this . firstKey ;
138- const nextKey = this . nextKey ;
139- let key : K | undefined ;
140- let started = false ;
195+ let keys = this . orderedKeys ;
196+ const map = this ; // eslint-disable-line @typescript-eslint/no-this-alias
197+ let idx = 1 ;
141198 return {
142199 [ Symbol . iterator ] ( ) : IterableIterator < K > {
143200 return this ;
144201 } ,
145202 next ( ) : IteratorResult < K > {
146- if ( ! started ) {
147- started = true ;
148- key = getFirstKey ( ) ;
149- } else {
150- key = nextKey . get ( key ! ) ;
203+ while ( keys . get ( 0 ) !== undefined ) {
204+ let adj = 0 ;
205+ let h = 1 ;
206+ while ( true ) {
207+ const holePos = keys . get ( - h ) as any as number | undefined ;
208+ if ( holePos === undefined || holePos >= idx ) break ;
209+ adj ++ ;
210+ h ++ ;
211+ }
212+ idx -= adj ;
213+ keys = keys . get ( 0 ) as any ;
151214 }
152- return { done : ! key , value : key ! } ;
215+ while ( idx < map . nextSlot && keys . get ( idx ) === undefined ) {
216+ idx ++ ;
217+ }
218+ if ( idx >= map . nextSlot ) {
219+ return { done : true , value : undefined ! } ;
220+ }
221+ const i = idx ;
222+ idx ++ ;
223+ return { done : false , value : keys . get ( i ) } ;
153224 } ,
154225 } ;
155226 }
156227
157228 public values ( ) : IterableIterator < V > {
158- const getFirstKey = ( ) => this . firstKey ;
159- const { items , nextKey } = this ;
160- let key : K | undefined ;
161- let started = false ;
229+ let keys = this . orderedKeys ;
230+ let vals = this . orderedValues ;
231+ const map = this ; // eslint-disable-line @typescript-eslint/no-this-alias
232+ let idx = 1 ;
162233 return {
163234 [ Symbol . iterator ] ( ) : IterableIterator < V > {
164235 return this ;
165236 } ,
166237 next ( ) : IteratorResult < V > {
167- if ( ! started ) {
168- started = true ;
169- key = getFirstKey ( ) ;
170- } else {
171- key = nextKey . get ( key ! ) ;
238+ while ( keys . get ( 0 ) !== undefined ) {
239+ let adj = 0 ;
240+ let h = 1 ;
241+ while ( true ) {
242+ const holePos = keys . get ( - h ) as any as number | undefined ;
243+ if ( holePos === undefined || holePos >= idx ) break ;
244+ adj ++ ;
245+ h ++ ;
246+ }
247+ idx -= adj ;
248+ vals = vals . get ( 0 ) as any ;
249+ keys = keys . get ( 0 ) as any ;
250+ }
251+ while ( idx < map . nextSlot && keys . get ( idx ) === undefined ) {
252+ idx ++ ;
253+ }
254+ if ( idx >= map . nextSlot ) {
255+ return { done : true , value : undefined ! } ;
172256 }
173- return { done : ! key , value : items . get ( key ! ) } ;
257+ const i = idx ;
258+ idx ++ ;
259+ return { done : false , value : vals . get ( i ) } ;
174260 } ,
175261 } ;
176262 }
0 commit comments