diff --git a/Jint/Native/Iterator/IteratorInstance.cs b/Jint/Native/Iterator/IteratorInstance.cs
index c410e6077..ed390e268 100644
--- a/Jint/Native/Iterator/IteratorInstance.cs
+++ b/Jint/Native/Iterator/IteratorInstance.cs
@@ -21,6 +21,24 @@ public override object ToObject()
public abstract bool TryIteratorStep(out ObjectInstance nextItem);
+ ///
+ /// Steps the iterator and hands back the value directly, letting concrete iterators skip
+ /// the per-step IteratorResult object when nothing user-visible needs it (for-in keys).
+ /// The default wraps with exactly the read the for-in/of
+ /// loop used to perform, so behavior is unchanged for every other iterator.
+ ///
+ internal virtual bool TryStepValue([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out JsValue? value)
+ {
+ if (!TryIteratorStep(out var result))
+ {
+ value = null;
+ return false;
+ }
+
+ value = result.Get(CommonProperties.Value);
+ return true;
+ }
+
///
/// https://tc39.es/ecma262/#sec-iteratornext
/// IteratorNext with an optional value argument, using the cached [[NextMethod]].
@@ -241,6 +259,157 @@ public override bool TryIteratorStep(out ObjectInstance nextItem)
}
}
+ ///
+ /// https://tc39.es/ecma262/#sec-enumerate-object-properties
+ /// for-in key enumerator: walks own string keys level by level up the prototype chain,
+ /// probing presence and enumerability per key at step time (a key deleted before its turn
+ /// is skipped). Keys seen as present on shallower levels are tracked in a plain list; the
+ /// hash set that shadow-filters deeper levels materializes only when a deeper level
+ /// actually produces a present key — enumerating an object whose prototypes contribute
+ /// nothing (the common case: every Object.prototype / Array.prototype member is
+ /// non-enumerable... but present, so they still probe) allocates no per-step result
+ /// objects and no nested per-level iterator machinery.
+ ///
+ internal sealed class ForInIterator : IteratorInstance
+ {
+ private ObjectInstance? _current;
+ private List _keys;
+ private int _index;
+ private bool _deeperLevel;
+ private List? _completedLevels;
+ private HashSet? _visited;
+
+ [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Auto)]
+ private readonly record struct CompletedLevel(ObjectInstance Owner, List Keys);
+
+ public ForInIterator(Engine engine, ObjectInstance target) : base(engine)
+ {
+ _current = target;
+ // matches the previous lazy enumerator's first step closely enough: no user code
+ // can observe between head evaluation and the first step, so the ownKeys order
+ // for proxies is preserved; GetPrototypeOf is deliberately NOT consulted here
+ // (a proxy trap must not fire before the first level is exhausted)
+ _keys = target.GetOwnPropertyKeys(Types.String);
+ }
+
+ internal override bool TryStepValue([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out JsValue? value)
+ {
+ while (_current is not null)
+ {
+ var current = _current;
+ var keys = _keys;
+ while (_index < keys.Count)
+ {
+ var key = keys[_index++];
+ var probe = current.ProbeOwnProperty(key);
+ if (probe == OwnPropertyProbe.Missing)
+ {
+ // deleted before its turn — not visited, does not shadow
+ continue;
+ }
+
+ if (_deeperLevel)
+ {
+ if (_visited is null)
+ {
+ if (probe != OwnPropertyProbe.Enumerable)
+ {
+ // present but non-enumerable: never yields, and its shadowing
+ // of still-deeper levels is reconstructed from the retained
+ // level snapshots if a set is ever needed
+ continue;
+ }
+
+ _visited = BuildVisited(current, keys, _index - 1);
+ }
+
+ if (!_visited.Add(key))
+ {
+ continue; // shadowed by a shallower level
+ }
+ }
+
+ if (probe == OwnPropertyProbe.Enumerable)
+ {
+ value = key;
+ return true;
+ }
+ }
+
+ var proto = current.GetPrototypeOf();
+ if (proto is null)
+ {
+ _current = null;
+ break;
+ }
+
+ if (_visited is null)
+ {
+ // retain the exhausted level's snapshot (a list reference, no copy) so a
+ // later set build can reconstruct what it shadowed
+ (_completedLevels ??= []).Add(new CompletedLevel(current, keys));
+ }
+
+ _deeperLevel = true;
+ _current = proto;
+ _keys = proto.GetOwnPropertyKeys(Types.String);
+ _index = 0;
+ }
+
+ value = null;
+ return false;
+ }
+
+ ///
+ /// First enumerable candidate on a deeper level: build the shadow set from every
+ /// shallower level's retained snapshot plus the already-processed prefix of the
+ /// current level, re-probing each key on its owning object. Prototype members are
+ /// overwhelmingly non-enumerable, so most enumerations never get here.
+ ///
+ private HashSet BuildVisited(ObjectInstance current, List currentKeys, int processedCount)
+ {
+ var set = new HashSet();
+ if (_completedLevels is not null)
+ {
+ foreach (var level in _completedLevels)
+ {
+ var levelKeys = level.Keys;
+ for (var i = 0; i < levelKeys.Count; i++)
+ {
+ if (level.Owner.ProbeOwnProperty(levelKeys[i]) != OwnPropertyProbe.Missing)
+ {
+ set.Add(levelKeys[i]);
+ }
+ }
+ }
+
+ _completedLevels = null;
+ }
+
+ for (var i = 0; i < processedCount; i++)
+ {
+ if (current.ProbeOwnProperty(currentKeys[i]) != OwnPropertyProbe.Missing)
+ {
+ set.Add(currentKeys[i]);
+ }
+ }
+
+ return set;
+ }
+
+ public override bool TryIteratorStep(out ObjectInstance nextItem)
+ {
+ if (TryStepValue(out var value))
+ {
+ nextItem = IteratorResult.CreateValueIteratorPosition(_engine, value);
+ return true;
+ }
+
+ nextItem = IteratorResult.CreateValueIteratorPosition(_engine, done: JsBoolean.True);
+ return false;
+ }
+ }
+
internal sealed class EnumerableIterator : IteratorInstance
{
private readonly IEnumerator _enumerable;
diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs
index 53940a743..96d5cdaec 100644
--- a/Jint/Native/Object/ObjectInstance.cs
+++ b/Jint/Native/Object/ObjectInstance.cs
@@ -2090,36 +2090,6 @@ public bool Equals(ObjectInstance? other)
public override int GetHashCode() => RuntimeHelpers.GetHashCode(this);
- internal IEnumerable GetKeys()
- {
- var visited = new HashSet();
- foreach (var key in GetOwnPropertyKeys(Types.String))
- {
- var probe = ProbeOwnProperty(key);
- if (probe != OwnPropertyProbe.Missing)
- {
- visited.Add(key);
- if (probe == OwnPropertyProbe.Enumerable)
- {
- yield return key;
- }
- }
- }
-
- if (Prototype is null)
- {
- yield break;
- }
-
- foreach (var protoKey in Prototype.GetKeys())
- {
- if (!visited.Contains(protoKey))
- {
- yield return protoKey;
- }
- }
- }
-
public override string ToString()
{
return TypeConverter.ToString(this);
diff --git a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
index 2ed0c1361..15c57344a 100644
--- a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
@@ -322,7 +322,7 @@ private bool HeadEvaluation(EvaluationContext context, [NotNullWhen(true)] out I
}
var obj = TypeConverter.ToObject(engine.Realm, exprValue);
- result = new IteratorInstance.EnumerableIterator(engine, obj.GetKeys());
+ result = new IteratorInstance.ForInIterator(engine, obj);
}
else if (_iterationKind == IterationKind.AsyncIterate)
{
@@ -428,11 +428,9 @@ private Completion BodyEvaluation(
asyncResumeData.CurrentValue = null;
resuming = false;
}
- else
+ else if (iteratorKind == IteratorKind.Async)
{
ObjectInstance nextResult;
-
- if (iteratorKind == IteratorKind.Async)
{
// For async iteration, we need to await the Promise from next()
// Note: We need direct access to async instances for state manipulation in SuspendForAsyncIteration
@@ -508,19 +506,23 @@ private Completion BodyEvaluation(
}
}
}
- else
+
+ nextValue = nextResult.Get(CommonProperties.Value);
+ }
+ else
+ {
+ // Sync iteration; TryStepValue skips the per-step IteratorResult for
+ // iterators that can (for-in keys) and is the same TryIteratorStep +
+ // Get(value) sequence for everything else.
+ if (!iteratorRecord.TryStepValue(out var steppedValue))
{
- // Sync iteration - use existing TryIteratorStep
- if (!iteratorRecord.TryIteratorStep(out nextResult))
- {
- close = true;
- // Clean up suspend data on normal completion
- suspendable?.Data.Clear(this);
- return new Completion(CompletionType.Normal, v, _statement!);
- }
+ close = true;
+ // Clean up suspend data on normal completion
+ suspendable?.Data.Clear(this);
+ return new Completion(CompletionType.Normal, v, _statement!);
}
- nextValue = nextResult.Get(CommonProperties.Value);
+ nextValue = steppedValue;
}
close = true;