Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions Jint/Native/Iterator/IteratorInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ public override object ToObject()

public abstract bool TryIteratorStep(out ObjectInstance nextItem);

/// <summary>
/// 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 <see cref="TryIteratorStep"/> with exactly the read the for-in/of
/// loop used to perform, so behavior is unchanged for every other iterator.
/// </summary>
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;
}

/// <summary>
/// https://tc39.es/ecma262/#sec-iteratornext
/// IteratorNext with an optional value argument, using the cached [[NextMethod]].
Expand Down Expand Up @@ -241,6 +259,157 @@ public override bool TryIteratorStep(out ObjectInstance nextItem)
}
}

/// <summary>
/// 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.
/// </summary>
internal sealed class ForInIterator : IteratorInstance
{
private ObjectInstance? _current;
private List<JsValue> _keys;
private int _index;
private bool _deeperLevel;
private List<CompletedLevel>? _completedLevels;
private HashSet<JsValue>? _visited;

[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Auto)]
private readonly record struct CompletedLevel(ObjectInstance Owner, List<JsValue> 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;
}

/// <summary>
/// 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.
/// </summary>
private HashSet<JsValue> BuildVisited(ObjectInstance current, List<JsValue> currentKeys, int processedCount)
{
var set = new HashSet<JsValue>();
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<JsValue> _enumerable;
Expand Down
30 changes: 0 additions & 30 deletions Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2090,36 +2090,6 @@ public bool Equals(ObjectInstance? other)

public override int GetHashCode() => RuntimeHelpers.GetHashCode(this);

internal IEnumerable<JsValue> GetKeys()
{
var visited = new HashSet<JsValue>();
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);
Expand Down
30 changes: 16 additions & 14 deletions Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Loading