-
Notifications
You must be signed in to change notification settings - Fork 1
Iterator Protocol
This page documents how Asynkron.JsEngine implements JavaScript's iterator and iterable protocols, including built-in iterators, for-of loops, and the TC39 iterator helpers.
flowchart TB
subgraph Iterable["Iterable Protocol"]
SymIter["[Symbol.iterator]()"]
Returns1["returns Iterator"]
end
subgraph Iterator["Iterator Protocol"]
Next["next()"]
Returns2["returns { value, done }"]
end
subgraph Result["IteratorResult"]
Value["value: any"]
Done["done: boolean"]
end
SymIter --> Returns1
Returns1 --> Iterator
Next --> Returns2
Returns2 --> Result
Iterable Protocol: An object is iterable if it has a [Symbol.iterator] method that returns an iterator.
Iterator Protocol: An iterator has a next() method that returns { value, done } objects.
The result of calling next() on an iterator:
File: JsTypes/IteratorResultObject.cs
internal sealed class IteratorResultObject : IJsObjectLike, IAsJsValue, IJsSurfacedMutable
{
// Singleton for exhausted iterators
public static readonly IteratorResultObject DoneUndefined = new(JsValue.Undefined, true);
private JsValue _value;
private bool _done;
public bool TryGetProperty(string name, out JsValue value)
{
switch (name)
{
case "value": value = _value; return true;
case "done": value = _done ? JsValue.True : JsValue.False; return true;
default: value = JsValue.Undefined; return false;
}
}
}Pooling: Iterator results are pooled via IteratorResultObjectPool to minimize allocations.
Base class for all built-in iterators:
File: JsTypes/JsIteratorBase.cs
public abstract class JsIteratorBase : IJsObjectLike, IAsJsValue, IPrototypeAccessorProvider
{
protected bool _done;
// All iterators are also iterable (return themselves)
// Symbol.iterator returns `this`
}File: Ast/Symbols.cs
public static class Symbols
{
public static readonly JsSymbol Iterator = JsSymbol.For("Symbol.iterator");
public static readonly JsSymbol AsyncIterator = JsSymbol.For("Symbol.asyncIterator");
public static readonly JsSymbol ToStringTag = JsSymbol.For("Symbol.toStringTag");
// ... other symbols
}Files:
JsTypes/JsArrayIterator.csStdLib/ArrayIterator/ArrayIteratorPrototype.csStdLib/Array/ArrayPrototype.Iterators.cs
internal sealed class JsArrayIterator : JsIteratorBase
{
internal enum ArrayIteratorKind { Entries, Keys, Values }
private readonly JsArray _array;
private readonly ArrayIteratorKind _kind;
private int _index;
internal JsValue Next()
{
if (_done) return IteratorResultObject.DoneUndefined.AsJsValue;
if (_index >= _array.Length)
{
_done = true;
return IteratorResultObject.DoneUndefined.AsJsValue;
}
var value = _kind switch
{
ArrayIteratorKind.Keys => JsValue.FromInt32(_index),
ArrayIteratorKind.Values => _array.GetElement(_index),
ArrayIteratorKind.Entries => CreateEntryPair(_index, _array.GetElement(_index)),
_ => throw new InvalidOperationException()
};
_index++;
return new IteratorResultObject(value, false).AsJsValue;
}
}Array Methods:
| Method | Returns |
|---|---|
arr.values() |
Iterator over values (default) |
arr.keys() |
Iterator over indices |
arr.entries() |
Iterator over [index, value] pairs |
arr[Symbol.iterator]() |
Same as values()
|
File: JsTypes/JsMapIterator.cs
internal sealed class JsMapIterator : JsIteratorBase
{
internal enum MapIteratorKind { Entries, Keys, Values }
private readonly JsMap _map;
private readonly IEnumerator<KeyValuePair<JsValue, JsValue>> _enumerator;
private readonly MapIteratorKind _kind;
}Map Methods:
| Method | Returns |
|---|---|
map.values() |
Iterator over values |
map.keys() |
Iterator over keys |
map.entries() |
Iterator over [key, value] pairs (default) |
File: JsTypes/JsSetIterator.cs
internal sealed class JsSetIterator : JsIteratorBase
{
internal enum SetIteratorKind { Entries, Values }
private readonly JsSet _set;
private readonly IEnumerator<JsValue> _enumerator;
private readonly SetIteratorKind _kind;
}Strings are iterable by code point (not UTF-16 code unit):
File: StdLib/Iteration/IterationHelper.cs
private static JsObject CreateStringIterator(string str)
{
// Per ES spec 22.1.5.2.1 %StringIteratorPrototype%.next():
// Strings are iterated by code point, not UTF-16 code unit.
// Surrogate pairs should be returned as a single string.
var index = 0;
return CreateIterator(() =>
{
if (index >= str.Length) return (JsValue.Undefined, true);
var cp = char.ConvertToUtf32(str, index);
var result = char.ConvertFromUtf32(cp);
index += result.Length; // 1 for BMP, 2 for surrogate pair
return (JsValue.FromString(result), false);
});
}// Surrogate pair example
const emoji = "😀"; // U+1F600, stored as surrogate pair
for (const char of emoji) {
console.log(char); // Logs "😀" once, not two surrogates
}File: Execution/Emitters/ForOfEmitter.cs
flowchart TB
subgraph ForOf["for (const x of iterable)"]
Init["IteratorInit\n(get iterator)"]
TryEnter["EnterTry"]
Loop["Loop Start"]
MoveNext["IteratorMoveNext"]
Check{done?}
Body["Loop Body"]
Finally["Finally Block"]
Close["IteratorClose"]
end
Init --> TryEnter
TryEnter --> Loop
Loop --> MoveNext
MoveNext --> Check
Check -->|no| Body
Body --> Loop
Check -->|yes| Finally
Finally --> Close
The for-of loop is lowered to IR instructions:
// ForOfEmitter.cs - Simplified
public void Emit(ForOfStatement node)
{
// 1. Get iterator from iterable
EmitIteratorInit(node.Right, isAsync: false);
// 2. Wrap in try/finally for proper cleanup
var tryIndex = EnterTry();
// 3. Loop: call next() and check done
var loopLabel = CreateLabel();
EmitIteratorMoveNext();
BranchIfDone(exitLabel);
// 4. Bind value to loop variable
EmitBinding(node.Left);
// 5. Execute body
EmitBody(node.Body);
Branch(loopLabel);
// 6. Finally: close iterator
EmitFinally();
EmitIteratorClose();
EndFinally();
}File: Ast/TypedAstEvaluator.ExecutionPlanRunner.Handlers.Iterators.cs
private void HandleIteratorInit(IteratorInitInstruction instruction)
{
var iterable = GetSlotValue(instruction.IterableSlot);
// Get Symbol.iterator method
if (!TryGetIteratorFromProtocols(iterable, out var iterator, out var enumerator))
{
throw ThrowTypeError("Value is not iterable", context);
}
// Store iterator state
SetSlotValue(instruction.IteratorSlot, iterator);
SetSlotValue(instruction.EnumeratorSlot, enumerator);
}
private void HandleIteratorMoveNext(IteratorMoveNextInstruction instruction)
{
var iterator = GetSlotValue(instruction.IteratorSlot);
var next = iterator.GetProperty("next");
var result = Call(next, iterator, []);
var done = result.GetProperty("done").IsTrue;
var value = result.GetProperty("value");
SetSlotValue(instruction.ValueSlot, value);
SetSlotValue(instruction.DoneSlot, done);
}
private void HandleIteratorClose(IteratorCloseInstruction instruction)
{
var iterator = GetSlotValue(instruction.IteratorSlot);
// Call return() if it exists
if (iterator.TryGetProperty("return", out var returnMethod))
{
Call(returnMethod, iterator, []);
}
}Per ECMA-262 §7.4.7, iterators should be closed when iteration ends early:
File: Ast/JsObjectExtensions.cs
internal static void IteratorClose(this IJsObjectLike iterator, EvaluationContext context,
bool preserveExistingThrow = false)
{
// If return() method exists, call it
if (iterator.TryGetProperty("return", out var returnMethod) && returnMethod.IsFunction)
{
var innerResult = Call(returnMethod, iterator, []);
// If preserveExistingThrow, restore the original exception after cleanup
}
}This ensures cleanup happens on:
-
breakstatements -
returnstatements inside loops - Thrown exceptions
The spread operator uses the iterator protocol:
File: Ast/TypedAstEvaluator.cs
// SpreadElement runtime semantics (ECMA-262 §12.2.5.2)
private static IEnumerable<JsValue> EnumerateSpread(JsValue value, EvaluationContext context)
{
if (!TryGetIteratorForDestructuring(value, context, out var iterator, out var enumerator))
{
throw ThrowTypeError("Value is not iterable", context);
}
var iteratorRecord = new ArrayPatternIterator(iterator, enumerator);
while (true)
{
var (item, done) = iteratorRecord.Next(context);
if (done) yield break;
yield return item;
}
}// Spread uses iterator protocol
const arr = [1, 2, 3];
const copy = [...arr]; // Array iterator
const str = [...'abc']; // String iterator: ['a', 'b', 'c']
const map = [...new Map([[1, 'a']])]; // Map iteratorAsynkron.JsEngine implements the TC39 Iterator Helpers proposal:
File: StdLib/Iterator/IteratorPrototype.cs
| Method | Description |
|---|---|
map(fn) |
Transform each value |
filter(fn) |
Keep values where fn returns true |
take(n) |
Take first n values |
drop(n) |
Skip first n values |
flatMap(fn) |
Map and flatten one level |
[JsHostMethod("map", Length = 1d)]
public JsValue Map(JsValue thisValue, IReadOnlyList<JsValue> args)
{
var iterator = GetIterator(thisValue);
var mapper = args.GetArgument(0);
return CreateIterator(() =>
{
var (value, done) = iterator.Next();
if (done) return (JsValue.Undefined, true);
var mapped = Call(mapper, JsValue.Undefined, [value]);
return (mapped, false);
});
}
[JsHostMethod("filter", Length = 1d)]
public JsValue Filter(JsValue thisValue, IReadOnlyList<JsValue> args)
{
var iterator = GetIterator(thisValue);
var predicate = args.GetArgument(0);
return CreateIterator(() =>
{
while (true)
{
var (value, done) = iterator.Next();
if (done) return (JsValue.Undefined, true);
if (Call(predicate, JsValue.Undefined, [value]).IsTrue)
{
return (value, false);
}
}
});
}
[JsHostMethod("take", Length = 1d)]
public JsValue Take(JsValue thisValue, IReadOnlyList<JsValue> args)
{
var iterator = GetIterator(thisValue);
var limit = (int)ToNumber(args.GetArgument(0));
var taken = 0;
return CreateIterator(() =>
{
if (taken >= limit) return (JsValue.Undefined, true);
var (value, done) = iterator.Next();
if (done) return (JsValue.Undefined, true);
taken++;
return (value, false);
});
}| Method | Description |
|---|---|
reduce(fn, init) |
Reduce to single value |
toArray() |
Collect into array |
forEach(fn) |
Execute callback for each |
some(fn) |
True if any match |
every(fn) |
True if all match |
find(fn) |
Find first matching |
[JsHostMethod("toArray", Length = 0d)]
public JsValue ToArray(JsValue thisValue, IReadOnlyList<JsValue> _)
{
var iterator = GetIterator(thisValue);
var result = new List<JsValue>();
while (true)
{
var (value, done) = iterator.Next();
if (done) break;
result.Add(value);
}
return JsArray.From(result);
}
[JsHostMethod("reduce", Length = 1d)]
public JsValue Reduce(JsValue thisValue, IReadOnlyList<JsValue> args)
{
var iterator = GetIterator(thisValue);
var reducer = args.GetArgument(0);
var accumulator = args.Count > 1 ? args[1] : GetFirstValue(iterator);
while (true)
{
var (value, done) = iterator.Next();
if (done) return accumulator;
accumulator = Call(reducer, JsValue.Undefined, [accumulator, value]);
}
}File: StdLib/Iterator/IteratorConstructor.cs
[JsHostMethod("from", Length = 1d)]
public JsValue From(JsValue _, IReadOnlyList<JsValue> args)
{
var value = args.GetArgument(0);
// If already an iterator, return as-is
if (value.TryGetProperty(Symbols.Iterator, out var iterMethod))
{
return Call(iterMethod, value, []);
}
throw ThrowTypeError("Value is not iterable", context);
}
[JsHostMethod("concat", Length = 0d)]
public JsValue Concat(JsValue _, IReadOnlyList<JsValue> args)
{
var iterables = args.ToArray();
var currentIndex = 0;
IEnumerator<JsValue>? currentIterator = null;
return CreateIterator(() =>
{
while (true)
{
if (currentIterator != null && currentIterator.MoveNext())
{
return (currentIterator.Current, false);
}
if (currentIndex >= iterables.Length)
{
return (JsValue.Undefined, true);
}
currentIterator = GetIterator(iterables[currentIndex++]);
}
});
}Iterators support the using declaration (TC39 Explicit Resource Management):
[JsHostMethod(SymbolKeys.Dispose, Length = 0d)]
public JsValue Dispose(JsValue thisValue, IReadOnlyList<JsValue> _)
{
// Close the iterator
IteratorClose(thisValue);
return JsValue.Undefined;
}// Using declaration auto-closes iterator
{
using iter = arr.values().take(5);
for (const x of iter) {
console.log(x);
}
} // iter[Symbol.dispose]() called hereFor for await...of loops:
File: StdLib/Iteration/IterationHelper.cs
public static JsValue GetAsyncIterator(JsValue value, EvaluationContext context)
{
// First check Symbol.asyncIterator
if (value.TryGetProperty(Symbols.AsyncIterator, out var asyncIterMethod))
{
return Call(asyncIterMethod, value, []);
}
// Fall back to Symbol.iterator and wrap in async
if (value.TryGetProperty(Symbols.Iterator, out var iterMethod))
{
var syncIterator = Call(iterMethod, value, []);
return WrapSyncIteratorAsAsync(syncIterator);
}
throw ThrowTypeError("Value is not async iterable", context);
}flowchart TB
subgraph ForAwaitOf["for await (const x of asyncIterable)"]
Init["Get async iterator"]
Loop["Loop Start"]
AwaitNext["await iterator.next()"]
Check{done?}
Body["Body"]
AwaitClose["await iterator.return()"]
end
Init --> Loop
Loop --> AwaitNext
AwaitNext --> Check
Check -->|no| Body
Body --> Loop
Check -->|yes| AwaitClose
Iterator results are pooled to reduce GC pressure:
internal static class IteratorResultObjectPool
{
private static readonly ObjectPool<IteratorResultObject> Pool = new(() => new());
public static IteratorResultObject Rent(JsValue value, bool done)
{
var obj = Pool.Get();
obj.Initialize(value, done);
return obj;
}
public static void Return(IteratorResultObject obj)
{
if (!obj.IsCaptured)
{
Pool.Return(obj);
}
}
}File: Ast/IteratorDriverPlanExtensions.cs
The iterator driver has optimizations for common patterns:
// Fast path: simple accumulator pattern
// for (const x of arr) sum += x;
if (IsSimpleAccumulatorPattern(body))
{
return ExecuteFastAccumulator(iterator, body);
}
// Fast path: direct slot binding
// for (const x of arr) { ... } // x is simple identifier
if (binding is IdentifierPattern)
{
// Direct slot write, no destructuring overhead
}The engine maintains both:
- Iterator protocol path (next/done/value objects)
- IEnumerator path for internal use
// Internal enumeration is faster
internal static IEnumerable<JsValue> EnumerateValues(JsValue iterable)
{
if (iterable.TryGetObject<JsArray>(out var arr))
{
// Fast path: direct array access
foreach (var item in arr.Items)
{
yield return item;
}
}
else
{
// Protocol path
var iterator = GetIterator(iterable);
// ... standard protocol
}
}| Category | File Path |
|---|---|
| IteratorResult | src/Asynkron.JsEngine/JsTypes/IteratorResultObject.cs |
| Base Class | src/Asynkron.JsEngine/JsTypes/JsIteratorBase.cs |
| Array Iterator | src/Asynkron.JsEngine/JsTypes/JsArrayIterator.cs |
| Map Iterator | src/Asynkron.JsEngine/JsTypes/JsMapIterator.cs |
| Set Iterator | src/Asynkron.JsEngine/JsTypes/JsSetIterator.cs |
| Symbols | src/Asynkron.JsEngine/Ast/Symbols.cs |
| Iteration Helpers | src/Asynkron.JsEngine/StdLib/Iteration/IterationHelper.cs |
| Iterator Prototype | src/Asynkron.JsEngine/StdLib/Iterator/IteratorPrototype.cs |
| Iterator Constructor | src/Asynkron.JsEngine/StdLib/Iterator/IteratorConstructor.cs |
| for-of Emitter | src/Asynkron.JsEngine/Execution/Emitters/ForOfEmitter.cs |
| Iterator Handlers | src/Asynkron.JsEngine/Ast/TypedAstEvaluator.ExecutionPlanRunner.Handlers.Iterators.cs |
| Iterator Driver | src/Asynkron.JsEngine/Execution/IteratorDriverPlan.cs |
| Iterator Close | src/Asynkron.JsEngine/Ast/JsObjectExtensions.cs |
| Result Pool | src/Asynkron.JsEngine/Pooling/IteratorResultObjectPool.cs |
- Generators-and-Async - Generator function implementation
- IR-Execution - How for-of is lowered to IR
- Pooling-Deep-Dive - Iterator result pooling details
- Symbol-System - Symbol.iterator and other well-known symbols