@swf2es/runtime 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,10 @@
5
5
  // Dictionary(true)) keep no key alive, as Flash's do not: their entries are
6
6
  // held strongly only by a WeakMap keyed by the key, and their slots hold
7
7
  // them weakly, so neither a for-in left off nor a value that refers to its
8
- // key keeps the key.
8
+ // key keeps the key. Reading a slot's WeakRef is the cost of a weak walk,
9
+ // so the entries one reads are kept beside the slots until the job ends,
10
+ // which is as long as the engine itself keeps what a WeakRef gave: a step's
11
+ // name and value, and the walks after it in that job, read none again.
9
12
 
10
13
  import type { AsObject, Value } from "./descriptors.js";
11
14
 
@@ -37,6 +40,24 @@ class WeakEntry extends Entry {
37
40
 
38
41
  type Held = Entry | WeakRef<Entry>;
39
42
 
43
+ /**
44
+ * The weak Dictionaries whose slots' entries are kept open in this job, and
45
+ * whether their closing at its end is asked for. deref() makes the engine
46
+ * keep its target until the job ends, so an entry kept that long keeps no
47
+ * key the walk that read it did not; kept longer, it would.
48
+ */
49
+ const openedInJob: DictionaryKeys[] = [];
50
+ let closing = false;
51
+
52
+ function closeOpened(): void {
53
+ for (let i = 0; i < openedInJob.length; i++) {
54
+ openedInJob[i].close();
55
+ }
56
+
57
+ openedInJob.length = 0;
58
+ closing = false;
59
+ }
60
+
40
61
  /**
41
62
  * As avmplus' hashtable, a name keeps its slot while it is there, so that a
42
63
  * for-in started inside another never moves the outer one's names. One
@@ -71,6 +92,12 @@ export class DictionaryKeys {
71
92
  private readonly spare: Entry[] = [];
72
93
  /** The slot of each dynamic property's name. */
73
94
  private named: Map<string, number> | null = null;
95
+ /**
96
+ * A weak Dictionary's entries that this job's for-ins have read from
97
+ * their slots, each at its slot's index, null where none was; null
98
+ * outside a job that walks it.
99
+ */
100
+ private opened: (Entry | null)[] | null = null;
74
101
 
75
102
  constructor(weak: boolean) {
76
103
  this.weak = weak;
@@ -171,6 +198,10 @@ export class DictionaryKeys {
171
198
  this.start(o);
172
199
  }
173
200
 
201
+ if (this.weak) {
202
+ return this.nextWeak(o, index);
203
+ }
204
+
174
205
  const slots = this.slots;
175
206
  for (let i = index; i < slots.length; i++) {
176
207
  const held = slots[i];
@@ -198,6 +229,64 @@ export class DictionaryKeys {
198
229
  return 0;
199
230
  }
200
231
 
232
+ /** As next, for weak keys: each slot's WeakRef is read once a job, then its entry kept. */
233
+ private nextWeak(o: AsObject, index: number): number {
234
+ const slots = this.slots;
235
+ const opened = this.opened ?? this.keepOpened();
236
+ for (let i = index; i < slots.length; i++) {
237
+ let entry = opened[i];
238
+ if (entry === null) {
239
+ const held = slots[i];
240
+ if (held === null) {
241
+ continue;
242
+ }
243
+
244
+ if (typeof held === "string") {
245
+ if (o.$d?.has(held) && !o.$dontEnum?.has(held)) {
246
+ return i + 1;
247
+ }
248
+
249
+ continue;
250
+ }
251
+
252
+ const read = (held as WeakRef<Entry>).deref();
253
+ if (read === undefined) {
254
+ slots[i] = null;
255
+ this.collected.push(i);
256
+ continue;
257
+ }
258
+
259
+ entry = read;
260
+ opened[i] = read;
261
+ }
262
+
263
+ if (entry.value !== GONE) {
264
+ return i + 1;
265
+ }
266
+ }
267
+
268
+ return 0;
269
+ }
270
+
271
+ /** Starts keeping the entries read from the slots, until the job ends. */
272
+ private keepOpened(): (Entry | null)[] {
273
+ const opened: (Entry | null)[] = new Array(this.slots.length).fill(null);
274
+ this.opened = opened;
275
+ openedInJob.push(this);
276
+ if (!closing) {
277
+ closing = true;
278
+ // A microtask runs before the job ends, when the engine lets go of what deref() gave.
279
+ void Promise.resolve().then(closeOpened);
280
+ }
281
+
282
+ return opened;
283
+ }
284
+
285
+ /** Lets go of the entries kept: the job that read them is over. */
286
+ close(): void {
287
+ this.opened = null;
288
+ }
289
+
201
290
  /** The name at a for-in's index: a key, a dynamic property's name, or "" once a key is gone. */
202
291
  nameAt(index: number): object | string {
203
292
  const held = this.slots[index - 1];
@@ -205,7 +294,7 @@ export class DictionaryKeys {
205
294
  return "";
206
295
  }
207
296
 
208
- return typeof held === "string" ? held : (this.open(held)?.key ?? "");
297
+ return typeof held === "string" ? held : (this.at(held, index - 1)?.key ?? "");
209
298
  }
210
299
 
211
300
  /** The value at a for-in's index: a key's, undefined once it is gone, or NAMED. */
@@ -219,10 +308,19 @@ export class DictionaryKeys {
219
308
  return NAMED;
220
309
  }
221
310
 
222
- const value = this.open(held)?.value;
311
+ const value = this.at(held, index - 1)?.value;
223
312
  return value === GONE ? undefined : value;
224
313
  }
225
314
 
315
+ /** The entry slot `i` holds: a weak one as this job's for-in read it, if it did. */
316
+ private at(held: Held, i: number): Entry | undefined {
317
+ if (!this.weak) {
318
+ return held as Entry;
319
+ }
320
+
321
+ return this.opened?.[i] ?? (held as WeakRef<Entry>).deref();
322
+ }
323
+
226
324
  /** The entry a slot or `added` holds, or undefined once its key is collected. */
227
325
  private open(held: Held): Entry | undefined {
228
326
  return this.weak ? (held as WeakRef<Entry>).deref() : (held as Entry);
@@ -249,8 +347,11 @@ export class DictionaryKeys {
249
347
  }
250
348
  }
251
349
 
252
- /** Those of `added` still there; the others are let go, their keys with them. */
253
- private settle(added: Held[]): Held[] {
350
+ /**
351
+ * Those of `added` still there; the others are let go, their keys with
352
+ * them. Their entries go into `entries`, for a caller to read none twice.
353
+ */
354
+ private settle(added: Held[], entries?: Entry[]): Held[] {
254
355
  let kept = 0;
255
356
  for (let i = 0; i < added.length; i++) {
256
357
  const held = added[i];
@@ -261,6 +362,7 @@ export class DictionaryKeys {
261
362
 
262
363
  if (entry.value !== GONE) {
263
364
  added[kept++] = held;
365
+ entries?.push(entry);
264
366
  } else {
265
367
  this.map.delete(entry.key as object);
266
368
  entry.key = null;
@@ -286,10 +388,16 @@ export class DictionaryKeys {
286
388
  }
287
389
 
288
390
  const slots = this.slots;
391
+ // In step with the slots: an entry freed is no longer its slot's, one given a slot is.
392
+ const opened = this.opened;
289
393
  const free: number[] = [];
290
394
  for (let i = 0; i < deleted.length; i++) {
291
395
  const entry = deleted[i];
292
396
  slots[entry.slot] = null;
397
+ if (opened !== null) {
398
+ opened[entry.slot] = null;
399
+ }
400
+
293
401
  free.push(entry.slot);
294
402
  entry.slot = -1;
295
403
  this.spare.push(entry);
@@ -322,15 +430,24 @@ export class DictionaryKeys {
322
430
  const i = next < free.length ? free[next++] : slots.length;
323
431
  slots[i] = name;
324
432
  this.named.set(name, i);
433
+ if (opened !== null) {
434
+ opened[i] = null;
435
+ }
325
436
  }
326
437
  }
327
438
  }
328
439
 
329
- const added = this.settle(this.added);
440
+ const entries: Entry[] = [];
441
+ const added = this.settle(this.added, entries);
330
442
  for (let n = 0; n < added.length; n++) {
331
443
  const i = next < free.length ? free[next++] : slots.length;
444
+ const entry = entries[n];
332
445
  slots[i] = added[n];
333
- (this.open(added[n]) as Entry).slot = i;
446
+ entry.slot = i;
447
+ if (opened !== null) {
448
+ // settle read it, so the engine keeps it for this job already.
449
+ opened[i] = entry;
450
+ }
334
451
  }
335
452
 
336
453
  // Kept for as many keys as were added since the last start, as the next
@@ -9,6 +9,7 @@ import { DictionaryKeys } from "./dictionary-keys.js";
9
9
  import { type DynamicProperties, GONE, type Walk } from "./dynamic-properties.js";
10
10
  import { Namespace } from "./names.js";
11
11
  import { FloatVector } from "./natives/float-vector.js";
12
+ import type { Traits } from "./traits.js";
12
13
 
13
14
  /**
14
15
  * What for-ins over one object keep apart from its names: elements are
@@ -36,6 +37,8 @@ interface SparseIndices {
36
37
  /** How many were given since they were taken, and whether they were taken again since this for-in met them. */
37
38
  sinceTaken: number;
38
39
  retaken: boolean;
40
+ /** Whether avmplus holds the Array's elements in its hashtable (see hashed). */
41
+ hashed: boolean;
39
42
  }
40
43
 
41
44
  /**
@@ -109,15 +112,50 @@ export function ownNames(o: AsObject): EnumeratedName[] {
109
112
  * The name of `o`, not a Dictionary, at a for-in's index among the names
110
113
  * past its elements, the index less their length.
111
114
  */
112
- export function enumerated(o: AsObject, at: number, index: number): string {
115
+ export function enumerated(o: AsObject, at: number, index: number): string | number {
113
116
  // The slots the step itself went through, without looking them up again.
114
117
  const walk =
115
118
  o === lastStep.o && index === lastStep.index && lastStep.walk !== null
116
119
  ? lastStep.walk
117
120
  : walkOf(o);
118
- return walk?.slots[at - 1] ?? "";
121
+ return walk?.slots[at - 1] ?? unslotted(index);
119
122
  }
120
123
 
124
+ /**
125
+ * The name at a for-in's index that no slot holds: an element's, where the
126
+ * index is negative, one a step by a sparse Array's indices gave (see
127
+ * stepBy), found here so that the names of every other step are looked up
128
+ * as before; else none.
129
+ */
130
+ function unslotted(index: number): string | number {
131
+ if (index >= 0) {
132
+ return "";
133
+ }
134
+
135
+ const element = -index - 1;
136
+ return element < INT_ATOM_LIMIT ? element : String(element);
137
+ }
138
+
139
+ /**
140
+ * The value of the element a step by a sparse Array's indices gave at
141
+ * for-in index `index`, negative, if `o` reads its elements as an Array
142
+ * does (`plain`) and it is still there; else `missing`.
143
+ */
144
+ export function sparseElement(o: AsObject, index: number, plain: boolean, missing: Value): Value {
145
+ const element = -index - 1;
146
+ if (plain) {
147
+ const value = o.$a[element];
148
+ if (value !== undefined || element in o.$a) {
149
+ return value;
150
+ }
151
+ }
152
+
153
+ return missing;
154
+ }
155
+
156
+ /** The name of the element at a for-in's index, as a property's. */
157
+ export const elementName = (index: number) => String(index < 0 ? -index - 1 : index - 1);
158
+
121
159
  function walkOf(o: AsObject): Walk | null {
122
160
  const d: DynamicProperties | null | undefined = o.$d;
123
161
  return d ? d.walk : null;
@@ -135,10 +173,29 @@ export function nextIndex(
135
173
  o: AsObject,
136
174
  index: number,
137
175
  ): number {
138
- // An object without elements never gains them: past its first step, no need to look again.
176
+ // What the last step read of the object stands: past its first step, no need to look again.
139
177
  const known = o === lastStep.o && index === lastStep.index;
140
- const a: Value[] | FloatVector | undefined = known && lastStep.none ? undefined : o.$a;
141
- if (!known) {
178
+ stepKnown = known;
179
+ let a: Value[] | FloatVector | undefined;
180
+ if (known) {
181
+ a = lastStep.elements;
182
+
183
+ // The element right after the last, there and not hidden: nearly every
184
+ // step. Not over an Array walked by its sparse indices, where the one
185
+ // after is a hole nearly every time.
186
+ if (
187
+ a !== undefined &&
188
+ lastStep.hidden === undefined &&
189
+ index < a.length &&
190
+ o !== sparseOwner &&
191
+ index in a
192
+ ) {
193
+ lastStep.index = index + 1;
194
+ return index + 1;
195
+ }
196
+ } else {
197
+ a = o.$a;
198
+ lastStep.elements = a;
142
199
  lastStep.walk = null;
143
200
  if (a !== undefined) {
144
201
  // Not the keys of a Dictionary whose for-in was left off.
@@ -151,13 +208,14 @@ export function nextIndex(
151
208
  ? nextOfNames(o, index, known)
152
209
  : nextOfElements(enumerating, o, a, index, known);
153
210
  if (next === 0) {
154
- lastStep.o = null;
155
- lastStep.keys = null;
156
- lastStep.walk = null;
211
+ letGo();
157
212
  } else {
158
213
  lastStep.o = o;
159
214
  lastStep.index = next;
160
- lastStep.none = a === undefined;
215
+ if (!known) {
216
+ lastStep.hidden = o.$dontEnum;
217
+ lastStep.traits = o.$traits;
218
+ }
161
219
  }
162
220
 
163
221
  return next;
@@ -188,27 +246,72 @@ export function dictionaryAt(o: AsObject, index: number): DictionaryKeys | null
188
246
  }
189
247
 
190
248
  /**
191
- * The step a for-in last took: the object, the index it gave, whether the
192
- * object has no elements and its keys if a Dictionary, so that the next
193
- * step and the nextName and nextValue right after it need not look for
194
- * them again, which misses, at a cost, on the many objects that have
195
- * neither, and never gain any. Let go when a for-in ends.
249
+ * The step a for-in last took: the object, the index it gave, and what the
250
+ * step read of the object: its elements, its hidden names, its traits and
251
+ * its keys if a Dictionary, so that the next step and the nextName and
252
+ * nextValue right after it need not read them again. The steps of every
253
+ * for-in go through this one code, over objects of every class, where the
254
+ * engine finds each of those by a lookup, several to a step: more than the
255
+ * step itself costs. Let go when a for-in ends.
256
+ *
257
+ * What it keeps must stand for as long as the object is the same: one
258
+ * without elements never gains them, and its traits and whether it is a
259
+ * Dictionary are for good; whoever gives an object other elements, or its
260
+ * first hidden name, says so (forgetStep).
196
261
  */
197
262
  const lastStep: {
198
263
  o: AsObject;
199
264
  index: number;
200
- none: boolean;
265
+ /** Its elements, undefined for an object without any. */
266
+ elements: Value[] | FloatVector | undefined;
267
+ /** Its names hidden from for-ins, undefined while it has had none. */
268
+ hidden: Set<string> | undefined;
269
+ traits: Traits | undefined;
201
270
  keys: DictionaryKeys | null;
202
271
  /** The slots of the names it went through, past any elements: the object's, as they never change. */
203
272
  walk: Walk | null;
204
273
  } = {
205
274
  o: null,
206
275
  index: 0,
207
- none: false,
276
+ elements: undefined,
277
+ hidden: undefined,
278
+ traits: undefined,
208
279
  keys: null,
209
280
  walk: null,
210
281
  };
211
282
 
283
+ /**
284
+ * Whether the step nextIndex is taking goes on from the last, so that the
285
+ * last one's hidden names are its object's: else they are read from the
286
+ * object, by a step that comes to ask, which one over an object with
287
+ * nothing to go through never does.
288
+ */
289
+ let stepKnown = false;
290
+
291
+ function hiddenOf(o: AsObject): Set<string> | undefined {
292
+ return stepKnown ? lastStep.hidden : o.$dontEnum;
293
+ }
294
+
295
+ /**
296
+ * `o` has other elements than it had (its storage, not what is in it), or
297
+ * its first hidden name: a for-in's next step over it reads them again.
298
+ */
299
+ export function forgetStep(o: AsObject): void {
300
+ if (o === lastStep.o) {
301
+ letGo();
302
+ }
303
+ }
304
+
305
+ /** The for-in is over: nothing of its object is kept, which would hold its class and all that hangs on it. */
306
+ function letGo(): void {
307
+ lastStep.o = null;
308
+ lastStep.elements = undefined;
309
+ lastStep.hidden = undefined;
310
+ lastStep.traits = undefined;
311
+ lastStep.keys = null;
312
+ lastStep.walk = null;
313
+ }
314
+
212
315
  /**
213
316
  * Whether a for-in's step over `o` at `index` goes on from the last one
214
317
  * nextIndex gave, so that `o` is an ordinary object: no hook, no QName.
@@ -217,13 +320,14 @@ export function continues(o: Value, index: number): boolean {
217
320
  return o === lastStep.o && index === lastStep.index;
218
321
  }
219
322
 
220
- /** The length of the elements of `o` at a for-in's index: where those end and its other names start. */
221
- export function elementsAt(o: AsObject, index: number): number {
222
- if (o === lastStep.o && index === lastStep.index && lastStep.none) {
223
- return 0;
224
- }
323
+ /** The elements of `o` at a for-in's index, whose length is where those end and its other names start. */
324
+ export function elementsAt(o: AsObject, index: number): Value[] | FloatVector | undefined {
325
+ return o === lastStep.o && index === lastStep.index ? lastStep.elements : o?.$a;
326
+ }
225
327
 
226
- return o?.$a?.length ?? 0;
328
+ /** The traits of `o` at a for-in's index. */
329
+ export function traitsAt(o: AsObject, index: number): Traits | undefined {
330
+ return o === lastStep.o && index === lastStep.index ? lastStep.traits : o?.$traits;
227
331
  }
228
332
 
229
333
  /** As nextIndex, over an object with elements. */
@@ -234,20 +338,24 @@ function nextOfElements(
234
338
  index: number,
235
339
  known: boolean,
236
340
  ): number {
237
- if (index === 0) {
238
- dropSparse(enumerating, o);
239
- walkOf(o)?.freeDeleted();
341
+ // The start of a for-in, or a step from a negative index: both off the
342
+ // path of a step over elements, which this one comparison is all of.
343
+ if (index <= 0) {
344
+ const next = startOrGoOn(enumerating, o, a, index, known);
345
+ if (index < 0) {
346
+ return next;
347
+ }
240
348
  }
241
349
 
242
350
  const length = a.length;
243
351
  if (index < length) {
244
- const i = nextElement(enumerating, o, a, index);
245
- if (i >= 0) {
246
- return i + 1;
352
+ const next = nextElement(enumerating, o, a, index);
353
+ if (next !== 0) {
354
+ return next;
247
355
  }
248
356
  }
249
357
 
250
- const at = Math.max(index - length, 0);
358
+ const at = index > length ? index - length : 0;
251
359
 
252
360
  // An Array that has never had other names needs no list of them.
253
361
  const next = at === 0 && !o.$d?.size ? 0 : nextNameIndex(o, at, false, known);
@@ -263,7 +371,11 @@ function nextOfElements(
263
371
  /** After this many holes in a row, the next element is found by an Array's indices, so that a sparse one stays cheap. */
264
372
  const HOLE_SCAN_LIMIT = 16;
265
373
 
266
- /** The first element of `o` at or after `from`, or -1. */
374
+ /**
375
+ * The index a for-in goes on from once it is given the first element of `o`
376
+ * at or after `from`, or 0 without one: one more than the element's, or
377
+ * that negated (see stepBy).
378
+ */
267
379
  function nextElement(
268
380
  enumerating: WeakMap<object, Enumeration>,
269
381
  o: AsObject,
@@ -272,24 +384,24 @@ function nextElement(
272
384
  ): number {
273
385
  const length = a.length;
274
386
  if (a instanceof FloatVector) {
275
- return from < length ? from : -1;
387
+ return from < length ? from + 1 : 0;
276
388
  }
277
389
 
278
390
  if (enumerable(o, a, from)) {
279
- return from;
391
+ return from + 1;
280
392
  }
281
393
 
282
394
  let e: Enumeration | undefined;
283
395
  if (sparseKept !== 0) {
284
396
  if (o === sparseOwner) {
285
- return nextSparse(o, a, ownerSparse as SparseIndices, from);
397
+ return stepBy(o, a, ownerSparse as SparseIndices, from);
286
398
  }
287
399
 
288
400
  e = enumerating.get(o);
289
401
  if (e?.sparse !== undefined) {
290
402
  sparseOwner = o;
291
403
  ownerSparse = e.sparse;
292
- return nextSparse(o, a, e.sparse, from);
404
+ return stepBy(o, a, e.sparse, from);
293
405
  }
294
406
  }
295
407
 
@@ -297,28 +409,104 @@ function nextElement(
297
409
  const scanned = Math.min(length, from + HOLE_SCAN_LIMIT);
298
410
  for (let i = from + 1; i < scanned; i++) {
299
411
  if (enumerable(o, a, i)) {
300
- return i;
412
+ return i + 1;
301
413
  }
302
414
  }
303
415
 
304
416
  if (scanned === length) {
305
- return -1;
417
+ return 0;
306
418
  }
307
419
 
420
+ return stepBy(o, a, takeSparse(enumerating, o, a, e), scanned);
421
+ }
422
+
423
+ /** Take the indices of sparse Array `o` for the for-in over it, which `e` keeps things for, if any. */
424
+ function takeSparse(
425
+ enumerating: WeakMap<object, Enumeration>,
426
+ o: AsObject,
427
+ a: Value[],
428
+ e: Enumeration | undefined,
429
+ ): SparseIndices {
308
430
  const keeper: Enumeration = e ?? enumerating.get(o) ?? {};
309
431
  enumerating.set(o, keeper);
310
432
  sparseKept++;
311
- keeper.sparse = {
312
- indices: indicesOf(a, 0),
313
- length,
433
+ const indices = indicesOf(a, 0);
434
+ const sparse: SparseIndices = {
435
+ indices,
436
+ length: a.length,
314
437
  at: -1,
315
438
  given: -1,
316
439
  sinceTaken: 0,
317
440
  retaken: false,
441
+ hashed: hashed(indices),
318
442
  };
443
+ keeper.sparse = sparse;
319
444
  sparseOwner = o;
320
- ownerSparse = keeper.sparse;
321
- return nextSparse(o, a, keeper.sparse, scanned);
445
+ ownerSparse = sparse;
446
+ return sparse;
447
+ }
448
+
449
+ /**
450
+ * The index a for-in goes on from after the first element of `o` at or
451
+ * after `from` by its sparse indices, or 0. Where avmplus walks the Array
452
+ * by index, that is one more than the element's, and the next step first
453
+ * asks whether the hole right after it was filled, as avmplus reaches one
454
+ * filled during the walk. Where avmplus walks its hashtable, such an
455
+ * element is reached or not by where it hashes, which no order of indices
456
+ * follows, and asking is a miss in the engine's dictionary-mode elements,
457
+ * a third of what a step cost: the index given is negative, the element's
458
+ * plus one negated, so that the next step comes straight back to the
459
+ * indices (startOrGoOn), told apart by the comparison with 0 every step
460
+ * makes already.
461
+ */
462
+ function stepBy(o: AsObject, a: Value[], sparse: SparseIndices, from: number): number {
463
+ const i = nextSparse(o, a, sparse, from);
464
+ if (i < 0) {
465
+ return 0;
466
+ }
467
+
468
+ return sparse.hashed ? -(i + 1) : i + 1;
469
+ }
470
+
471
+ /**
472
+ * What nextOfElements does first at a for-in's start, index 0: the indices
473
+ * and the deleted names' slots a for-in left off kept are let go. From a
474
+ * negative index, one stepBy gave, the whole step, by the indices alone,
475
+ * then the Array's other names.
476
+ */
477
+ function startOrGoOn(
478
+ enumerating: WeakMap<object, Enumeration>,
479
+ o: AsObject,
480
+ a: Value[] | FloatVector,
481
+ index: number,
482
+ known: boolean,
483
+ ): number {
484
+ if (index === 0) {
485
+ dropSparse(enumerating, o);
486
+ walkOf(o)?.freeDeleted();
487
+ return 0;
488
+ }
489
+
490
+ const from = -index;
491
+ let next = 0;
492
+ if (from < a.length) {
493
+ if (o === sparseOwner) {
494
+ next = stepBy(o, a as Value[], ownerSparse as SparseIndices, from);
495
+ } else {
496
+ const sparse = sparseKept !== 0 ? enumerating.get(o)?.sparse : undefined;
497
+ if (sparse !== undefined) {
498
+ sparseOwner = o;
499
+ ownerSparse = sparse;
500
+ next = stepBy(o, a as Value[], sparse, from);
501
+ } else {
502
+ // Dropped when another for-in over the Array started: taken again from here.
503
+ next = nextElement(enumerating, o, a, from);
504
+ }
505
+ }
506
+ }
507
+
508
+ // Past the last element, as a step from the Array's length.
509
+ return next !== 0 ? next : nextOfElements(enumerating, o, a, a.length, known);
322
510
  }
323
511
 
324
512
  /**
@@ -378,12 +566,32 @@ function retake(a: Value[], sparse: SparseIndices): void {
378
566
  sparse.at = -1;
379
567
  sparse.given = -1;
380
568
  sparse.sinceTaken = 0;
569
+ // An Array in avmplus' hashtable never leaves it.
570
+ sparse.hashed ||= hashed(sparse.indices);
571
+ }
572
+
573
+ /**
574
+ * Whether avmplus holds an Array with these elements in its hashtable, as
575
+ * it must when they span more than 32 slots and more than four times one
576
+ * more than their count (ArrayObject's shouldBeSparse): its dense part
577
+ * never holds so few over so many. Else avmplus may hold them dense,
578
+ * walking them by index, as the for-in here does.
579
+ */
580
+ function hashed(indices: number[]): boolean {
581
+ const n = indices.length;
582
+ if (n === 0) {
583
+ return false;
584
+ }
585
+
586
+ const span = indices[n - 1] - indices[0] + 1;
587
+ return span > 32 && span > (n + 1) * 4;
381
588
  }
382
589
 
383
590
  /** An Array's indices from `from` on, in order, as Object.keys gives them, appended to `indices`. */
384
591
  function indicesOf(a: Value[], from: number, indices: number[] = []): number[] {
385
- for (const k of Object.keys(a)) {
386
- const i = Number(k);
592
+ const keys = Object.keys(a);
593
+ for (let k = 0; k < keys.length; k++) {
594
+ const i = Number(keys[k]);
387
595
  if (i >= from) {
388
596
  indices.push(i);
389
597
  }
@@ -408,8 +616,10 @@ function firstAtOrAfter(indices: number[], from: number): number {
408
616
  return low;
409
617
  }
410
618
 
619
+ /** Whether element `i` of `o`, the object nextIndex steps over, is there and not hidden. */
411
620
  function enumerable(o: AsObject, a: Value[], i: number): boolean {
412
- return i in a && !o.$dontEnum?.has(String(i));
621
+ const hidden = hiddenOf(o);
622
+ return i in a && (hidden === undefined || !hidden.has(String(i)));
413
623
  }
414
624
 
415
625
  /**
@@ -437,8 +647,9 @@ function nextNameIndex(o: AsObject, index: number, starting: boolean, known: boo
437
647
  // A name deleted since the for-in started is passed over.
438
648
  const slots = walk.slots;
439
649
  const live = walk.live;
650
+ const hidden = hiddenOf(o);
440
651
  for (let i = index; i < slots.length; i++) {
441
- if (live[i] !== GONE && !o.$dontEnum?.has(slots[i] as string)) {
652
+ if (live[i] !== GONE && (hidden === undefined || !hidden.has(slots[i] as string))) {
442
653
  return i + 1;
443
654
  }
444
655
  }
@@ -237,7 +237,13 @@ export const arrayNatives: Natives = {
237
237
  "Array#set:length": (rt) =>
238
238
  function (this: AsObject, n: Value) {
239
239
  if (!sealed(rt, this, 0)) {
240
- this.$a.length = rt.toUint(n);
240
+ // Array.prototype.push sets the length its elements already gave it, at
241
+ // every call: the engine's setter is a call into its runtime even then.
242
+ const a: Value[] = this.$a;
243
+ const length = rt.toUint(n);
244
+ if (length !== a.length) {
245
+ a.length = length;
246
+ }
241
247
  }
242
248
  },
243
249
  [`Array#${AS3}::push`]: (rt) =>