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;