diff --git a/Jint/AstExtensions.cs b/Jint/AstExtensions.cs
index aef2acd7b7..1bde5ce247 100644
--- a/Jint/AstExtensions.cs
+++ b/Jint/AstExtensions.cs
@@ -82,7 +82,11 @@ or NodeType.ArrayExpression
or NodeType.ObjectExpression)
{
var context = engine._activeEvaluationContext ?? new EvaluationContext(engine);
- return JintExpression.Build(expression).GetValue(context);
+ var result = JintExpression.Build(expression).GetValue(context);
+
+ // If the expression suspended the generator (e.g., yield in computed property name),
+ // return the value. The caller should check ExecutionContext.Suspended.
+ return result;
}
return JsValue.Undefined;
diff --git a/Jint/Native/Function/ClassDefinition.cs b/Jint/Native/Function/ClassDefinition.cs
index 5b20af05d2..bda9e316a1 100644
--- a/Jint/Native/Function/ClassDefinition.cs
+++ b/Jint/Native/Function/ClassDefinition.cs
@@ -166,6 +166,13 @@ public JsValue BuildConstructor(EvaluationContext context, Environment env)
var target = !isStatic ? proto : F;
var element = ClassElementEvaluation(engine, target, e);
+
+ // Check for generator suspension after evaluating class element
+ if (engine.ExecutionContext.Suspended)
+ {
+ return JsValue.Undefined;
+ }
+
if (element is PrivateElement privateElement)
{
var container = !isStatic ? instancePrivateMethods : staticPrivateMethods;
@@ -258,6 +265,12 @@ private static ClassFieldDefinition ClassFieldDefinitionEvaluation(Engine engine
{
var name = fieldDefinition.GetKey(engine);
+ // Check for generator suspension after evaluating computed property key
+ if (engine.ExecutionContext.Suspended)
+ {
+ return new ClassFieldDefinition { Name = JsValue.Undefined, Initializer = null };
+ }
+
ScriptFunction? initializer = null;
if (fieldDefinition.Value is not null)
{
@@ -366,6 +379,13 @@ public ClassStaticBlockFunction(StaticBlock staticBlock) : base(NodeType.StaticB
var intrinsics = engine.Realm.Intrinsics;
var value = method.TryGetKey(engine);
+
+ // Check for generator suspension after evaluating computed property key
+ if (engine.ExecutionContext.Suspended)
+ {
+ return null;
+ }
+
var propKey = TypeConverter.ToPropertyKey(value);
var env = engine.ExecutionContext.LexicalEnvironment;
var privateEnv = engine.ExecutionContext.PrivateEnvironment;
diff --git a/Jint/Native/Generator/GeneratorInstance.cs b/Jint/Native/Generator/GeneratorInstance.cs
index d630cea464..d6754e1f79 100644
--- a/Jint/Native/Generator/GeneratorInstance.cs
+++ b/Jint/Native/Generator/GeneratorInstance.cs
@@ -71,15 +71,11 @@ internal sealed class GeneratorInstance : ObjectInstance
internal ObjectInstance? _delegationInnerResult;
///
- /// When set, indicates that the generator should complete immediately with this value.
- /// Used by yield* when the inner iterator's return method is null/undefined.
+ /// Signals that generator.return() was called and execution should complete
+ /// after running finally blocks. Aligns with spec's Return completion handling
+ /// in GeneratorResumeAbrupt.
///
- internal JsValue? _earlyReturnValue;
-
- ///
- /// Whether an early return was triggered (e.g., from yield* with null return method).
- ///
- internal bool _shouldEarlyReturn;
+ internal bool _returnRequested;
///
/// The completion type used when resuming via GeneratorResumeAbrupt.
@@ -106,18 +102,11 @@ internal sealed class GeneratorInstance : ObjectInstance
internal object? _currentFinallyStatement;
///
- /// Maps for-of/for-in statement nodes to their suspended iterator state.
- /// When a generator yields inside a for-of loop, the iterator is stored here
- /// so it can be restored on resume instead of creating a new one.
- ///
- internal Dictionary? _forOfSuspendData;
-
- ///
- /// Maps array destructuring pattern nodes to their suspended iterator state.
- /// When a generator yields inside array destructuring (e.g., [x[yield]] = iterable),
- /// the iterator is stored here so it can be properly closed on generator.return().
+ /// Unified dictionary for all suspend data (for-of loops, destructuring patterns, etc.).
+ /// Keys are Jint expression/statement instances (not AST nodes) to avoid collisions
+ /// when the same script runs on multiple Engine instances.
///
- internal Dictionary? _destructuringSuspendData;
+ internal Dictionary? _suspendData;
public GeneratorInstance(Engine engine) : base(engine)
{
@@ -220,16 +209,6 @@ private ObjectInstance ResumeExecution(in ExecutionContext genContext, Evaluatio
var result = _generatorBody.Execute(context);
_engine.LeaveExecutionContext();
- // Check for early return (e.g., from yield* with null return method)
- if (_shouldEarlyReturn)
- {
- var earlyValue = _earlyReturnValue ?? JsValue.Undefined;
- _shouldEarlyReturn = false;
- _earlyReturnValue = null;
- _generatorState = GeneratorState.Completed;
- return IteratorResult.CreateValueIteratorPosition(_engine, earlyValue, done: JsBoolean.True);
- }
-
ObjectInstance? resultValue = null;
if (result.Type == CompletionType.Normal)
{
@@ -285,77 +264,41 @@ private GeneratorState GeneratorValidate(JsValue? generatorBrand)
}
///
- /// Gets or creates suspend data for a for-of statement.
- /// Called when a generator is about to execute a for-of loop body.
- ///
- internal ForOfSuspendData GetOrCreateForOfSuspendData(object statement, IteratorInstance iterator)
- {
- _forOfSuspendData ??= new Dictionary();
- if (!_forOfSuspendData.TryGetValue(statement, out var data))
- {
- data = new ForOfSuspendData { Iterator = iterator };
- _forOfSuspendData[statement] = data;
- }
- return data;
- }
-
- ///
- /// Tries to get existing suspend data for a for-of statement.
- /// Returns true if we're resuming into this for-of loop.
- ///
- internal bool TryGetForOfSuspendData(object statement, out ForOfSuspendData? data)
- {
- if (_forOfSuspendData?.TryGetValue(statement, out data) == true)
- {
- return true;
- }
- data = null;
- return false;
- }
-
- ///
- /// Clears suspend data for a for-of statement when the loop completes normally.
- ///
- internal void ClearForOfSuspendData(object statement)
- {
- _forOfSuspendData?.Remove(statement);
- }
-
- ///
- /// Gets or creates suspend data for an array destructuring pattern.
- /// Called when a generator is about to execute destructuring that may contain yields.
+ /// Gets or creates suspend data of the specified type.
+ /// Keys should be Jint expression/statement instances (this) to avoid collisions across engines.
///
- internal DestructuringSuspendData GetOrCreateDestructuringSuspendData(object pattern, IteratorInstance iterator)
+ internal T GetOrCreateSuspendData(object key, IteratorInstance iterator) where T : SuspendData, new()
{
- _destructuringSuspendData ??= new Dictionary();
- if (!_destructuringSuspendData.TryGetValue(pattern, out var data))
+ _suspendData ??= new Dictionary();
+ if (!_suspendData.TryGetValue(key, out var data))
{
- data = new DestructuringSuspendData { Iterator = iterator };
- _destructuringSuspendData[pattern] = data;
+ data = new T { Iterator = iterator };
+ _suspendData[key] = data;
}
- return data;
+ return (T) data;
}
///
- /// Tries to get existing suspend data for an array destructuring pattern.
- /// Returns true if we're resuming into this destructuring.
+ /// Tries to get existing suspend data of the specified type.
+ /// Returns true if suspend data exists for the given key.
///
- internal bool TryGetDestructuringSuspendData(object pattern, out DestructuringSuspendData? data)
+ internal bool TryGetSuspendData(object key, out T? data) where T : SuspendData
{
- if (_destructuringSuspendData?.TryGetValue(pattern, out data) == true)
+ if (_suspendData?.TryGetValue(key, out var baseData) == true)
{
+ data = (T) baseData;
return true;
}
- data = null;
+ data = default;
return false;
}
///
- /// Clears suspend data for an array destructuring pattern when it completes.
+ /// Clears suspend data for the given key when the construct completes.
///
- internal void ClearDestructuringSuspendData(object pattern)
+ internal void ClearSuspendData(object key)
{
- _destructuringSuspendData?.Remove(pattern);
+ _suspendData?.Remove(key);
}
///
@@ -365,20 +308,18 @@ internal void ClearDestructuringSuspendData(object pattern)
///
internal void CloseAllDestructuringIterators(CompletionType completionType)
{
- if (_destructuringSuspendData is null)
+ if (_suspendData is null)
{
return;
}
- foreach (var kvp in _destructuringSuspendData)
+ foreach (var kvp in _suspendData)
{
- var data = kvp.Value;
- if (!data.Done)
+ if (kvp.Value is DestructuringSuspendData data && !data.Done)
{
data.Iterator.Close(completionType);
data.Done = true;
}
}
- _destructuringSuspendData.Clear();
}
}
diff --git a/Jint/Runtime/DestructuringSuspendData.cs b/Jint/Runtime/DestructuringSuspendData.cs
deleted file mode 100644
index afd5ff2dc4..0000000000
--- a/Jint/Runtime/DestructuringSuspendData.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Jint.Native;
-using Jint.Native.Iterator;
-
-namespace Jint.Runtime;
-
-///
-/// Stores the state of an array destructuring pattern when a generator yields inside it.
-/// When a generator yields during array destructuring (e.g., [x[yield]] = iterable),
-/// the iterator must be preserved so it can be properly closed when the generator
-/// completes or returns.
-///
-internal sealed class DestructuringSuspendData
-{
- ///
- /// The iterator instance from the destructuring that was in progress.
- ///
- public IteratorInstance Iterator { get; init; } = null!;
-
- ///
- /// The current element index in the destructuring pattern.
- ///
- public uint ElementIndex { get; set; }
-
- ///
- /// Whether the iterator has been exhausted (done=true).
- ///
- public bool Done { get; set; }
-
- ///
- /// Values already retrieved from the iterator for each element position.
- /// Used when resuming to avoid calling next() again.
- ///
- public JsValue[]? RetrievedValues { get; set; }
-}
diff --git a/Jint/Runtime/ForOfSuspendData.cs b/Jint/Runtime/ForOfSuspendData.cs
deleted file mode 100644
index 9e20f2898e..0000000000
--- a/Jint/Runtime/ForOfSuspendData.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Jint.Native;
-using Jint.Native.Iterator;
-using Jint.Runtime.Environments;
-
-namespace Jint.Runtime;
-
-///
-/// Stores the state of a for-of/for-in loop when a generator yields inside it.
-/// Following the NiL.JS SuspendData pattern for iterator state preservation.
-///
-internal sealed class ForOfSuspendData
-{
- ///
- /// The iterator instance from HeadEvaluation that was in progress.
- ///
- public IteratorInstance Iterator { get; init; } = null!;
-
- ///
- /// The current value being processed (from TryIteratorStep).
- /// Needed when yield happens during destructuring or body execution.
- ///
- public JsValue? CurrentValue { get; set; }
-
- ///
- /// The accumulated result value (v) from previous iterations.
- ///
- public JsValue AccumulatedValue { get; set; } = JsValue.Undefined;
-
- ///
- /// The iteration environment for lexical bindings (let/const in for-of).
- ///
- public DeclarativeEnvironment? IterationEnv { get; set; }
-}
diff --git a/Jint/Runtime/GeneratorReturnException.cs b/Jint/Runtime/GeneratorReturnException.cs
deleted file mode 100644
index 4894c735ba..0000000000
--- a/Jint/Runtime/GeneratorReturnException.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Jint.Native;
-
-namespace Jint.Runtime;
-
-///
-/// Internal exception used to implement generator return() behavior.
-/// When return() is called on a suspended generator, this exception is thrown
-/// to trigger finally blocks and iterator close operations before completing.
-///
-internal sealed class GeneratorReturnException : Exception
-{
- public JsValue ReturnValue { get; }
-
- public GeneratorReturnException(JsValue returnValue) : base()
- {
- ReturnValue = returnValue;
- }
-}
diff --git a/Jint/Runtime/Interpreter/EvaluationContext.cs b/Jint/Runtime/Interpreter/EvaluationContext.cs
index c65d96c6b2..4fc0c80531 100644
--- a/Jint/Runtime/Interpreter/EvaluationContext.cs
+++ b/Jint/Runtime/Interpreter/EvaluationContext.cs
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
+using Jint.Native.Generator;
namespace Jint.Runtime.Interpreter;
@@ -27,6 +28,25 @@ public EvaluationContext()
public readonly Engine Engine;
public bool DebugMode => Engine._isDebugMode;
+ ///
+ /// Returns true if the generator is suspended (yielded) or a return was requested.
+ /// This is the combined check that should be used after evaluating sub-expressions.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsGeneratorAborted()
+ {
+ var generator = Engine.ExecutionContext.Generator;
+ return generator is not null &&
+ (generator._generatorState == GeneratorState.SuspendedYield || generator._returnRequested);
+ }
+
+ ///
+ /// Returns true if the generator is suspended (yielded).
+ /// Use this when you only need to check for yield suspension without return request.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsGeneratorSuspended() => Engine.ExecutionContext.Suspended;
+
public Node LastSyntaxElement
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/Jint/Runtime/Interpreter/Expressions/DestructuringPatternAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/DestructuringPatternAssignmentExpression.cs
index 567ec7ccb7..7dc0e6211e 100644
--- a/Jint/Runtime/Interpreter/Expressions/DestructuringPatternAssignmentExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/DestructuringPatternAssignmentExpression.cs
@@ -92,7 +92,7 @@ private static JsValue HandleArrayPattern(
var resuming = false;
if (generator is not null && generator._isResuming)
{
- if (generator.TryGetDestructuringSuspendData(pattern, out suspendData))
+ if (generator.TryGetSuspendData(pattern, out suspendData))
{
resuming = true;
}
@@ -117,9 +117,11 @@ private static JsValue HandleArrayPattern(
suspendData.Done = true;
iterator.Close(CompletionType.Return);
}
- generator.ClearDestructuringSuspendData(pattern);
- // Throw to propagate the return
- throw new GeneratorReturnException(generator._nextValue ?? JsValue.Undefined);
+ generator.ClearSuspendData(pattern);
+ // Signal return request - callers check _returnRequested flag
+ generator._returnRequested = true;
+ generator._suspendedValue = generator._nextValue ?? JsValue.Undefined;
+ return JsValue.Undefined;
}
}
else
@@ -136,7 +138,7 @@ private static JsValue HandleArrayPattern(
// Save the iterator for potential yield inside this pattern
if (generator is not null && iterator is not null)
{
- suspendData = generator.GetOrCreateDestructuringSuspendData(pattern, iterator);
+ suspendData = generator.GetOrCreateSuspendData(pattern, iterator);
}
}
}
@@ -188,6 +190,26 @@ private static JsValue HandleArrayPattern(
close = true;
var reference = GetReferenceFromMember(context, me);
+ // Check for generator suspension after evaluating member expression
+ if (context.IsGeneratorSuspended())
+ {
+ close = false; // Don't close iterator, we'll resume later
+ return JsValue.Undefined;
+ }
+
+ // Check for generator return request
+ if (generator?._returnRequested == true)
+ {
+ if (!done && iterator is not null)
+ {
+ done = true;
+ iterator.Close(CompletionType.Return);
+ }
+ generator.ClearSuspendData(pattern);
+ close = false; // Already closed
+ return JsValue.Undefined;
+ }
+
JsValue value;
if (arrayOperations != null)
{
@@ -221,6 +243,26 @@ private static JsValue HandleArrayPattern(
if (restElement.Argument is MemberExpression memberExpression)
{
reference = GetReferenceFromMember(context, memberExpression);
+
+ // Check for generator suspension after evaluating member expression
+ if (context.IsGeneratorSuspended())
+ {
+ close = false; // Don't close iterator, we'll resume later
+ return JsValue.Undefined;
+ }
+
+ // Check for generator return request
+ if (generator?._returnRequested == true)
+ {
+ if (!done && iterator is not null)
+ {
+ done = true;
+ iterator.Close(CompletionType.Return);
+ }
+ generator.ClearSuspendData(pattern);
+ close = false; // Already closed
+ return JsValue.Undefined;
+ }
}
JsArray array;
@@ -283,6 +325,26 @@ private static JsValue HandleArrayPattern(
{
var jintExpression = Build(assignmentPattern.Right);
value = jintExpression.GetValue(context);
+
+ // Check for generator suspension after evaluating default value
+ if (context.IsGeneratorSuspended())
+ {
+ close = false; // Don't close iterator, we'll resume later
+ return JsValue.Undefined;
+ }
+
+ // Check for generator return request after evaluating default value
+ if (generator?._returnRequested == true)
+ {
+ if (!done && iterator is not null)
+ {
+ done = true;
+ iterator.Close(CompletionType.Return);
+ }
+ generator.ClearSuspendData(pattern);
+ close = false; // Already closed
+ return JsValue.Undefined;
+ }
}
if (assignmentPattern.Left is Identifier leftIdentifier)
@@ -304,36 +366,39 @@ private static JsValue HandleArrayPattern(
Throw.ArgumentOutOfRangeException(nameof(pattern), $"Unable to determine how to handle array pattern element {left}");
break;
}
+
+ // Check for generator suspension after processing each element
+ if (context.IsGeneratorSuspended())
+ {
+ // Generator yield - don't close the iterator, we'll resume later
+ close = false;
+ return JsValue.Undefined;
+ }
+
+ // Check for generator return request
+ if (generator?._returnRequested == true)
+ {
+ // Generator return() was called - close iterator with Return completion
+ if (!done && iterator is not null)
+ {
+ done = true; // Prevent double-close in finally
+ iterator.Close(CompletionType.Return);
+ }
+ generator.ClearSuspendData(pattern);
+ close = false; // Prevent double-close in finally
+ return JsValue.Undefined;
+ }
}
close = true;
// Clear suspend data on normal completion
- generator?.ClearDestructuringSuspendData(pattern);
- }
- catch (YieldSuspendException)
- {
- // Generator yield - don't close the iterator, we'll resume later
- close = false;
- throw;
- }
- catch (GeneratorReturnException)
- {
- // Generator return() was called - close iterator with Return completion
- // This allows TypeErrors from iterator.return() to propagate properly
- if (!done && iterator is not null)
- {
- done = true; // Prevent double-close in finally
- iterator.Close(CompletionType.Return);
- }
- // Clear suspend data
- generator?.ClearDestructuringSuspendData(pattern);
- throw;
+ generator?.ClearSuspendData(pattern);
}
catch
{
completionType = CompletionType.Throw;
// Clear suspend data on error
- generator?.ClearDestructuringSuspendData(pattern);
+ generator?.ClearSuspendData(pattern);
throw;
}
finally
diff --git a/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs b/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs
index 6b854df188..fa6c767ee5 100644
--- a/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs
+++ b/Jint/Runtime/Interpreter/Expressions/ExpressionCache.cs
@@ -86,6 +86,13 @@ internal void BuildArguments(EvaluationContext context, JsValue[] targetArray)
for (uint i = 0; i < (uint) expressions.Length; i++)
{
targetArray[i] = GetValue(context, expressions[i])!;
+
+ // Check for generator suspension after each expression evaluation
+ // This is needed because yield expressions return normally instead of throwing
+ if (context.IsGeneratorSuspended())
+ {
+ return;
+ }
}
}
@@ -147,6 +154,14 @@ internal void BuildArgumentsWithSpreads(EvaluationContext context, List
if (expression is JintSpreadExpression jse)
{
jse.GetValueAndCheckIterator(context, out var objectInstance, out var iterator);
+
+ // If generator suspended during spread expression evaluation, stop processing
+ // The iterator will be null because we haven't started iteration yet
+ if (context.IsGeneratorSuspended())
+ {
+ return;
+ }
+
// optimize for array unless someone has touched the iterator
if (objectInstance is JsArray { HasOriginalIterator: true } ai)
{
@@ -166,6 +181,12 @@ internal void BuildArgumentsWithSpreads(EvaluationContext context, List
else
{
target.Add(GetValue(context, expression)!);
+
+ // Check for generator suspension after each expression evaluation
+ if (context.IsGeneratorSuspended())
+ {
+ return;
+ }
}
}
}
diff --git a/Jint/Runtime/Interpreter/Expressions/JintArrayExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintArrayExpression.cs
index dbc2e574ae..acb77a2545 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintArrayExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintArrayExpression.cs
@@ -40,11 +40,26 @@ protected override object EvaluateInternal(EvaluationContext context)
{
var values = new JsValue[expressions.Length];
_arguments.BuildArguments(context, values);
+
+ // If generator suspended during argument evaluation, return undefined
+ // The expression will be re-evaluated on resume
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
+
return new JsArray(engine, values);
}
var array = new List();
_arguments.BuildArgumentsWithSpreads(context, array);
+
+ // If generator suspended during argument evaluation, return undefined
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
+
return new JsArray(engine, array.ToArray());
}
diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
index 51b5bf4d31..e5342811df 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
@@ -410,6 +410,13 @@ private JsValue SetValue(EvaluationContext context)
var rval = _right.GetValue(context);
+ // If generator suspended or return requested during right-hand side evaluation, don't assign
+ if (context.IsGeneratorAborted())
+ {
+ engine._referencePool.Return(lref);
+ return rval;
+ }
+
engine.PutValue(lref, rval);
engine._referencePool.Return(lref);
return rval;
@@ -441,6 +448,12 @@ private JsValue SetValue(EvaluationContext context)
return completion;
}
+ // If generator suspended or return requested during right-hand side evaluation, don't assign
+ if (context.IsGeneratorAborted())
+ {
+ return completion;
+ }
+
var rval = completion.Clone();
if (right._expression.IsFunctionDefinition())
diff --git a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
index 319ef640cf..e827499a5c 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintBinaryExpression.cs
@@ -647,8 +647,21 @@ protected override object EvaluateInternal(EvaluationContext context)
EnsureInitialized();
var left = _left.GetValue(context);
+
+ // Check for generator suspension after evaluating left operand
+ if (context.IsGeneratorSuspended())
+ {
+ return left;
+ }
+
var right = _right.GetValue(context);
+ // Check for generator suspension after evaluating right operand
+ if (context.IsGeneratorSuspended())
+ {
+ return right;
+ }
+
var oi = right as ObjectInstance;
if (oi is null)
{
diff --git a/Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs
index ea6fc20dd8..a5798793c8 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintCallExpression.cs
@@ -48,6 +48,12 @@ protected override object EvaluateInternal(EvaluationContext context)
var reference = _calleeExpression.Evaluate(context);
+ // Check for generator suspension after evaluating callee
+ if (context.IsGeneratorSuspended())
+ {
+ return reference as JsValue ?? JsValue.Undefined;
+ }
+
if (ReferenceEquals(reference, JsValue.Undefined))
{
return JsValue.Undefined;
@@ -106,6 +112,16 @@ protected override object EvaluateInternal(EvaluationContext context)
var arguments = this._arguments.ArgumentListEvaluation(context, out var rented);
+ // Check for generator suspension after argument evaluation
+ if (context.IsGeneratorSuspended())
+ {
+ if (rented && arguments is not null)
+ {
+ engine._jsValueArrayPool.ReturnArray(arguments);
+ }
+ return func; // Return any value, caller will check Suspended
+ }
+
if (!func.IsObject() && !engine._referenceResolver.TryGetCallable(engine, reference, out func))
{
ThrowMemberIsNotFunction(referenceRecord, reference, engine);
diff --git a/Jint/Runtime/Interpreter/Expressions/JintConditionalExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintConditionalExpression.cs
index b23d05eb88..b6f09dca51 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintConditionalExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintConditionalExpression.cs
@@ -15,7 +15,15 @@ public JintConditionalExpression(ConditionalExpression expression) : base(expres
protected override object EvaluateInternal(EvaluationContext context)
{
- return TypeConverter.ToBoolean(_test.GetValue(context))
+ var testValue = _test.GetValue(context);
+
+ // Check for generator suspension after evaluating test
+ if (context.IsGeneratorSuspended())
+ {
+ return testValue;
+ }
+
+ return TypeConverter.ToBoolean(testValue)
? _consequent.GetValue(context)
: _alternate.GetValue(context);
}
diff --git a/Jint/Runtime/Interpreter/Expressions/JintLogicalAndExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintLogicalAndExpression.cs
index 5a3239ac99..b4920b524b 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintLogicalAndExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintLogicalAndExpression.cs
@@ -29,6 +29,12 @@ protected override object EvaluateInternal(EvaluationContext context)
var left = _left.GetValue(context);
+ // Check for generator suspension after evaluating left operand
+ if (context.IsGeneratorSuspended())
+ {
+ return left;
+ }
+
if (left is JsBoolean b && !b._value)
{
return b;
diff --git a/Jint/Runtime/Interpreter/Expressions/JintLogicalOrExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintLogicalOrExpression.cs
index 96a7053832..39d874a78f 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintLogicalOrExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintLogicalOrExpression.cs
@@ -17,6 +17,12 @@ protected override object EvaluateInternal(EvaluationContext context)
{
var left = _left.GetValue(context);
+ // Check for generator suspension after evaluating left operand
+ if (context.IsGeneratorSuspended())
+ {
+ return left;
+ }
+
if (left is JsBoolean b && b._value)
{
return b;
diff --git a/Jint/Runtime/Interpreter/Expressions/JintObjectExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintObjectExpression.cs
index c3d5751fcb..33522307ef 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintObjectExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintObjectExpression.cs
@@ -134,14 +134,22 @@ protected override object EvaluateInternal(EvaluationContext context)
///
/// Version that can safely build plain object with only normal init/data fields fast.
///
- private JsObject BuildObjectFast(EvaluationContext context)
+ private JsValue BuildObjectFast(EvaluationContext context)
{
- var obj = new JsObject(context.Engine);
+ var engine = context.Engine;
+ var obj = new JsObject(engine);
var properties = new PropertyDictionary(_properties.Length, checkExistingKeys: true);
for (var i = 0; i < _properties.Length; i++)
{
var objectProperty = _properties[i];
var propValue = _valueExpressions.GetValue(context, i);
+
+ // Check for generator suspension after each property evaluation
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
+
properties[objectProperty!._key!] = new PropertyDescriptor(propValue, PropertyFlag.ConfigurableEnumerableWritable);
}
@@ -164,7 +172,15 @@ private object BuildObjectNormal(EvaluationContext context)
if (objectProperty is null)
{
// spread
- if (_valueExpressions.GetValue(context, i) is ObjectInstance source)
+ var spreadValue = _valueExpressions.GetValue(context, i);
+
+ // Check for generator suspension
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
+
+ if (spreadValue is ObjectInstance source)
{
source.CopyDataProperties(obj, excludedItems: null);
}
@@ -189,12 +205,25 @@ private object BuildObjectNormal(EvaluationContext context)
return value;
}
+ // Check for generator suspension after evaluating computed property key
+ if (context.IsGeneratorSuspended())
+ {
+ return value;
+ }
+
propName = TypeConverter.ToPropertyKey(value);
}
if (property.Kind == PropertyKind.Init)
{
var propValue = _valueExpressions.GetValue(context, i)!;
+
+ // Check for generator suspension
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
+
if (string.Equals(objectProperty._key, "__proto__", StringComparison.Ordinal) && !objectProperty._value.Computed && !objectProperty._value.Shorthand)
{
if (propValue.IsObject() || propValue.IsNull())
diff --git a/Jint/Runtime/Interpreter/Expressions/JintSequenceExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintSequenceExpression.cs
index 15078e1b6a..96aaf20ef9 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintSequenceExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintSequenceExpression.cs
@@ -36,6 +36,12 @@ protected override object EvaluateInternal(EvaluationContext context)
foreach (var expression in _expressions)
{
result = expression.GetValue(context);
+
+ // Check for generator suspension after each expression
+ if (context.IsGeneratorSuspended())
+ {
+ return result;
+ }
}
return result;
diff --git a/Jint/Runtime/Interpreter/Expressions/JintSpreadExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintSpreadExpression.cs
index 03ea123fd6..193d851e22 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintSpreadExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintSpreadExpression.cs
@@ -32,6 +32,14 @@ public override JsValue GetValue(EvaluationContext context)
internal void GetValueAndCheckIterator(EvaluationContext context, out JsValue instance, out IteratorInstance? iterator)
{
instance = _argument.GetValue(context);
+
+ // If generator suspended during argument evaluation, don't try to get iterator
+ if (context.IsGeneratorSuspended())
+ {
+ iterator = null;
+ return;
+ }
+
if (instance is null || !instance.TryGetIterator(context.Engine.Realm, out iterator))
{
iterator = null;
diff --git a/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs
index ccea4e8dbb..0264854df8 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs
@@ -47,12 +47,14 @@ protected override object EvaluateInternal(EvaluationContext context)
Throw.JavaScriptException(context.Engine, returnValue, AstExtensions.DefaultLocation);
}
- // If we're resuming with a Return completion, throw to trigger finally blocks
- // This allows for-of loops to close their iterators properly
+ // If we're resuming with a Return completion, signal return request
+ // This allows for-of loops to close their iterators properly via finally blocks
if (generator._resumeCompletionType == CompletionType.Return)
{
generator._resumeCompletionType = CompletionType.Normal; // Reset for future
- throw new GeneratorReturnException(returnValue);
+ generator._returnRequested = true;
+ generator._suspendedValue = returnValue;
+ return returnValue; // Callers check _returnRequested flag
}
// Store this value for future iterations (e.g., same yield in a loop)
@@ -78,6 +80,13 @@ protected override object EvaluateInternal(EvaluationContext context)
if (expression.Argument is not null)
{
value = Build(expression.Argument).GetValue(context);
+
+ // If the argument evaluation suspended the generator (nested yield), propagate
+ // the suspension up without yielding again - the inner yield already suspended
+ if (context.IsGeneratorSuspended())
+ {
+ return value;
+ }
}
else
{
@@ -191,6 +200,12 @@ private static JsValue RunYieldDelegateLoop(
// Yield the value from the inner iterator and suspend
// Per spec, pass innerResult directly to GeneratorYield to preserve its exact state
SuspendForDelegation(context, generator, innerResult, CompletionType.Normal);
+
+ // Check if suspended - if so, return to propagate suspension up the call stack
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
}
}
else if (receivedType == CompletionType.Throw)
@@ -230,6 +245,12 @@ private static JsValue RunYieldDelegateLoop(
{
// Yield the result and suspend
SuspendForDelegation(context, generator, innerObj, CompletionType.Normal);
+
+ // Check if suspended - if so, return to propagate suspension up the call stack
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
}
}
else
@@ -263,15 +284,16 @@ private static JsValue RunYieldDelegateLoop(
}
// Per spec: "Return Completion(received)" - the generator should complete
- // with the received return value, not just return it from yield*
+ // with the received return value, but we must let try-finally blocks execute.
+ // Use _returnRequested to signal return completion through normal execution flow.
generator._delegatingIterator = null;
generator._delegatingYieldNode = null;
- generator._generatorState = GeneratorState.Completed;
- generator._shouldEarlyReturn = true;
- generator._earlyReturnValue = temp;
+ generator._returnRequested = true;
+ generator._suspendedValue = temp;
- // Throw to interrupt execution - ResumeExecution will see the early return flag
- throw new YieldSuspendException(temp);
+ // Return to let the normal execution flow handle the return,
+ // which will trigger any finally blocks before completing
+ return temp;
}
var innerReturnResult = returnMethod.Call(iteratorInstance, new[] { receivedValue });
@@ -294,9 +316,12 @@ private static JsValue RunYieldDelegateLoop(
var returnValue = IteratorValue(innerReturnObj);
generator._delegatingIterator = null;
generator._delegatingYieldNode = null;
- // Throw GeneratorReturnException to signal Return completion
+
+ // Signal return request - callers check _returnRequested flag
// This will trigger finally blocks and then complete the generator
- throw new GeneratorReturnException(returnValue);
+ generator._returnRequested = true;
+ generator._suspendedValue = returnValue;
+ return returnValue;
}
if (generatorKind == GeneratorKind.Async)
@@ -309,6 +334,12 @@ private static JsValue RunYieldDelegateLoop(
{
// Yield the result and suspend
SuspendForDelegation(context, generator, innerReturnObj, CompletionType.Normal);
+
+ // Check if suspended - if so, return to propagate suspension up the call stack
+ if (context.IsGeneratorSuspended())
+ {
+ return JsValue.Undefined;
+ }
}
}
}
@@ -316,6 +347,7 @@ private static JsValue RunYieldDelegateLoop(
///
/// Suspends the generator during yield* delegation.
+ /// Sets generator state to suspendedYield - callers check context.IsGeneratorSuspended().
///
private static void SuspendForDelegation(
EvaluationContext context,
@@ -331,8 +363,7 @@ private static void SuspendForDelegation(
// Store the inner result to return it directly (preserving its exact 'done' property state)
generator._delegationInnerResult = innerResult;
- // Throw to suspend - don't extract value, just signal suspension
- throw new YieldSuspendException(JsValue.Undefined);
+ // Return normally - callers check ExecutionContext.Suspended flag
}
private static bool IteratorComplete(JsValue iterResult)
@@ -370,6 +401,7 @@ private static ObjectInstance Await(JsValue innerResult)
///
/// https://tc39.es/ecma262/#sec-yield
+ /// https://tc39.es/ecma262/#sec-generatoryield
///
private static JsValue Yield(EvaluationContext context, JsValue iterNextObj)
{
@@ -381,16 +413,18 @@ private static JsValue Yield(EvaluationContext context, JsValue iterNextObj)
Throw.NotImplementedException("async not implemented");
}
- // https://tc39.es/ecma262/#sec-generatoryield
+ // GeneratorYield per spec:
+ // 1. Set generator.[[GeneratorState]] to suspendedYield
var genContext = engine.ExecutionContext;
var generator = genContext.Generator;
generator!._generatorState = GeneratorState.SuspendedYield;
+
// Store the yielded value so it can be retrieved even if the containing statement
// has a different completion value (e.g., variable declarations return Empty)
generator._suspendedValue = iterNextObj;
- // Throw an exception to immediately interrupt expression evaluation.
- // This is caught at the statement level to handle generator suspension properly.
- throw new YieldSuspendException(iterNextObj);
+ // Return normally - callers check ExecutionContext.Suspended flag
+ // to detect that the generator has yielded
+ return iterNextObj;
}
}
diff --git a/Jint/Runtime/Interpreter/Expressions/NullishCoalescingExpression.cs b/Jint/Runtime/Interpreter/Expressions/NullishCoalescingExpression.cs
index e808a0efa7..fa879190bc 100644
--- a/Jint/Runtime/Interpreter/Expressions/NullishCoalescingExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/NullishCoalescingExpression.cs
@@ -41,6 +41,12 @@ private JsValue EvaluateConstantOrExpression(EvaluationContext context)
{
var left = _left.GetValue(context);
+ // Check for generator suspension after evaluating left operand
+ if (context.IsGeneratorSuspended())
+ {
+ return left;
+ }
+
return !left.IsNullOrUndefined()
? left
: _constant ?? _right!.GetValue(context);
diff --git a/Jint/Runtime/Interpreter/JintStatementList.cs b/Jint/Runtime/Interpreter/JintStatementList.cs
index 45fd3e20fc..c2929cce3b 100644
--- a/Jint/Runtime/Interpreter/JintStatementList.cs
+++ b/Jint/Runtime/Interpreter/JintStatementList.cs
@@ -100,19 +100,27 @@ public Completion Execute(EvaluationContext context)
}
// Check for generator suspension - works for both the main generator body and nested blocks
- if (context.Engine.ExecutionContext.Suspended)
+ var gen = context.Engine.ExecutionContext.Generator;
+ if (context.IsGeneratorSuspended())
{
// Don't increment _index - we'll re-execute this statement on resume
// The yield tracking (_yieldIndex) handles knowing which yield to resume from
_index = i;
// Use the suspended value from the generator, as the statement's completion value
// might be different (e.g., variable declarations return Empty, not the yielded value)
- var generator = context.Engine.ExecutionContext.Generator;
- var suspendedValue = generator?._suspendedValue ?? c.Value;
+ var suspendedValue = gen?._suspendedValue ?? c.Value;
// Return directly - don't fall through to the reset below
return new Completion(CompletionType.Return, suspendedValue, pair.Statement._statement);
}
+ // Check for generator return request (from generator.return() call)
+ if (gen?._returnRequested == true)
+ {
+ Reset();
+ var returnValue = gen._suspendedValue ?? c.Value;
+ return new Completion(CompletionType.Return, returnValue, pair.Statement._statement);
+ }
+
// With node-based yield tracking, we don't need to reset state between statements
// Each yield node is uniquely identified, so no per-statement cleanup is needed
@@ -132,21 +140,6 @@ public Completion Execute(EvaluationContext context)
// (e.g., this block is a for-of body that will execute again on next iteration)
_index = 0;
}
- catch (YieldSuspendException yieldEx)
- {
- // Generator yield - set index to current statement for resume
- _index = i;
- var generator = context.Engine.ExecutionContext.Generator;
- var suspendedValue = generator?._suspendedValue ?? yieldEx.YieldedValue;
- c = new Completion(CompletionType.Return, suspendedValue, temp[i].Statement._statement);
- }
- catch (GeneratorReturnException returnEx)
- {
- // Generator return() was called - propagate as Return completion
- // This allows for-of loops to close their iterators properly
- Reset();
- c = new Completion(CompletionType.Return, returnEx.ReturnValue, temp[i].Statement._statement);
- }
catch (Exception ex)
{
Reset();
diff --git a/Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs b/Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
index d3cf4650f3..1c53e6e9ad 100644
--- a/Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
@@ -87,18 +87,6 @@ private Completion ExecuteSingle(EvaluationContext context)
blockValue = JintStatementList.HandleError(context.Engine, _singleStatement);
}
}
- catch (YieldSuspendException yieldEx)
- {
- // Generator yield - propagate as Return completion
- var generator = context.Engine.ExecutionContext.Generator;
- var suspendedValue = generator?._suspendedValue ?? yieldEx.YieldedValue;
- blockValue = new Completion(CompletionType.Return, suspendedValue, _singleStatement!._statement);
- }
- catch (GeneratorReturnException returnEx)
- {
- // Generator return() was called - propagate as Return completion
- blockValue = new Completion(CompletionType.Return, returnEx.ReturnValue, _singleStatement!._statement);
- }
catch (Exception ex)
{
if (ex is JintException)
@@ -111,6 +99,21 @@ private Completion ExecuteSingle(EvaluationContext context)
}
}
+ // Check for generator suspension
+ var gen = context.Engine.ExecutionContext.Generator;
+ if (context.IsGeneratorSuspended())
+ {
+ var suspendedValue = gen?._suspendedValue ?? blockValue.Value;
+ return new Completion(CompletionType.Return, suspendedValue, _singleStatement!._statement);
+ }
+
+ // Check for generator return request
+ if (gen?._returnRequested == true)
+ {
+ var returnValue = gen._suspendedValue ?? blockValue.Value;
+ return new Completion(CompletionType.Return, returnValue, _singleStatement!._statement);
+ }
+
return blockValue;
}
diff --git a/Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs b/Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs
index 0b81cf9c30..8d99d6795d 100644
--- a/Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintDoWhileStatement.cs
@@ -37,7 +37,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
}
// Check for generator suspension - if the generator is suspended, we need to exit the loop
- if (context.Engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
var generator = context.Engine.ExecutionContext.Generator;
var suspendedValue = generator?._suspendedValue ?? completion.Value;
diff --git a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
index 58b18bff32..c92dd9b862 100644
--- a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
@@ -105,7 +105,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
if (generator is not null && generator._isResuming)
{
- if (generator.TryGetForOfSuspendData(_statement!, out suspendData))
+ if (generator.TryGetSuspendData(this, out suspendData))
{
// We're resuming into this for-of loop - use the saved iterator
keyResult = suspendData!.Iterator;
@@ -219,7 +219,7 @@ private Completion BodyEvaluation(
{
close = true;
// Clean up suspend data on normal completion
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
return new Completion(CompletionType.Normal, v, _statement!);
}
@@ -294,6 +294,25 @@ private Completion BodyEvaluation(
iterationEnv,
checkPatternPropertyReference: _lhsKind != LhsKind.VarBinding);
+ // Check for generator suspension after destructuring
+ if (context.IsGeneratorSuspended())
+ {
+ close = false; // Don't close iterator, we'll resume later
+ completionType = CompletionType.Return;
+ return new Completion(CompletionType.Return, generator?._suspendedValue ?? nextValue, _statement!);
+ }
+
+ // Check for generator return request after destructuring
+ if (generator?._returnRequested == true)
+ {
+ completionType = CompletionType.Return;
+ close = false; // Prevent double-close in finally
+ generator.ClearSuspendData(this);
+ iteratorRecord.Close(completionType);
+ var returnValue = generator._suspendedValue ?? nextValue;
+ return new Completion(CompletionType.Return, returnValue, _statement!);
+ }
+
status = context.Completion;
if (lhsKind == LhsKind.Assignment)
@@ -315,7 +334,7 @@ private Completion BodyEvaluation(
if (status != CompletionType.Normal)
{
engine.UpdateLexicalEnvironment(oldEnv);
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
if (_iterationKind == IterationKind.AsyncIterate)
{
iteratorRecord.Close(status);
@@ -334,7 +353,7 @@ private Completion BodyEvaluation(
// Before executing body, save state in case of yield
if (generator is not null)
{
- var data = generator.GetOrCreateForOfSuspendData(_statement!, iteratorRecord);
+ var data = generator.GetOrCreateSuspendData(this, iteratorRecord);
data.AccumulatedValue = v;
data.CurrentValue = nextValue;
data.IterationEnv = iterationEnv;
@@ -343,9 +362,9 @@ private Completion BodyEvaluation(
var result = stmt.Execute(context);
// Clear current value after successful body execution (not suspended)
- if (generator is not null && !engine.ExecutionContext.Suspended)
+ if (generator is not null && !context.IsGeneratorSuspended())
{
- if (generator.TryGetForOfSuspendData(_statement!, out var currentData))
+ if (generator.TryGetSuspendData(this, out var currentData))
{
currentData!.CurrentValue = null;
}
@@ -358,14 +377,14 @@ private Completion BodyEvaluation(
{
v = result.Value;
// Update accumulated value in suspend data
- if (generator is not null && generator.TryGetForOfSuspendData(_statement!, out var data))
+ if (generator is not null && generator.TryGetSuspendData(this, out var data))
{
data!.AccumulatedValue = v;
}
}
// Check for generator suspension - if the generator is suspended, we need to exit the loop
- if (engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
// Iterator is already saved in suspend data, just exit
close = false; // Don't close - we'll resume
@@ -374,10 +393,22 @@ private Completion BodyEvaluation(
return new Completion(CompletionType.Return, suspendedValue, _statement!);
}
+ // Check for generator return request (generator.return() was called)
+ if (generator?._returnRequested == true)
+ {
+ // Close iterator with Return completion
+ completionType = CompletionType.Return;
+ close = false; // Prevent double-close in finally
+ generator.ClearSuspendData(this);
+ iteratorRecord.Close(completionType);
+ var returnValue = generator._suspendedValue ?? result.Value;
+ return new Completion(CompletionType.Return, returnValue, _statement!);
+ }
+
if (result.Type == CompletionType.Break && (context.Target == null || string.Equals(context.Target, _statement?.LabelSet?.Name, StringComparison.Ordinal)))
{
completionType = CompletionType.Normal;
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
return new Completion(CompletionType.Normal, v, _statement!);
}
@@ -387,41 +418,23 @@ private Completion BodyEvaluation(
if (result.IsAbrupt())
{
close = true;
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
return result;
}
}
}
}
- catch (YieldSuspendException)
- {
- // Generator yield - don't close the iterator, we'll resume later
- // Suspend data is already saved, so iterator state is preserved
- close = false;
- throw;
- }
- catch (GeneratorReturnException)
- {
- // Generator return() was called - close iterator with Return completion
- // This allows TypeErrors from iterator.return() to propagate properly
- completionType = CompletionType.Return;
- close = false; // Prevent double-close in finally
- generator?.ClearForOfSuspendData(_statement!);
- iteratorRecord.Close(completionType);
- // Re-throw to continue unwinding (will be caught by JintStatementList)
- throw;
- }
catch
{
completionType = CompletionType.Throw;
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
throw;
}
finally
{
if (close)
{
- generator?.ClearForOfSuspendData(_statement!);
+ generator?.ClearSuspendData(this);
try
{
iteratorRecord.Close(completionType);
diff --git a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
index c6bba6b06e..9272f5e33c 100644
--- a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs
@@ -195,7 +195,7 @@ private Completion ForBodyEvaluation(EvaluationContext context)
}
// Check for generator suspension - if the generator is suspended, we need to exit the loop
- if (context.Engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
var generator = context.Engine.ExecutionContext.Generator;
var suspendedValue = generator?._suspendedValue ?? result.Value;
@@ -223,17 +223,23 @@ private Completion ForBodyEvaluation(EvaluationContext context)
if (_increment != null)
{
debugHandler?.OnStep(_increment._expression);
- try
- {
- _increment.Evaluate(context);
- }
- catch (YieldSuspendException yieldEx)
+ _increment.Evaluate(context);
+
+ // Check for generator suspension in update expression (e.g., yield in the update)
+ if (context.IsGeneratorSuspended())
{
- // Generator yielded in the update expression - return with the yielded value
var generator = context.Engine.ExecutionContext.Generator;
- var suspendedValue = generator?._suspendedValue ?? yieldEx.YieldedValue;
+ var suspendedValue = generator?._suspendedValue ?? JsValue.Undefined;
return new Completion(CompletionType.Return, suspendedValue, ((JintStatement) this)._statement);
}
+
+ // Check for generator return request
+ var gen = context.Engine.ExecutionContext.Generator;
+ if (gen?._returnRequested == true)
+ {
+ var returnValue = gen._suspendedValue ?? JsValue.Undefined;
+ return new Completion(CompletionType.Return, returnValue, ((JintStatement) this)._statement);
+ }
}
}
}
diff --git a/Jint/Runtime/Interpreter/Statements/JintTryStatement.cs b/Jint/Runtime/Interpreter/Statements/JintTryStatement.cs
index 3a03507ae2..ca3d9221d3 100644
--- a/Jint/Runtime/Interpreter/Statements/JintTryStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintTryStatement.cs
@@ -57,7 +57,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
// If a generator is suspended (yield), don't run the finally yet.
// The finally will run when the generator resumes and exits the try block.
- if (engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
return b;
}
@@ -77,6 +77,14 @@ protected override Completion ExecuteInternal(EvaluationContext context)
}
}
+ // Clear _returnRequested before running finally block.
+ // Per ECMAScript spec, a return in the finally block supersedes any pending return.
+ // If we don't clear this, the finally block's statements will incorrectly use _suspendedValue.
+ if (generator is not null)
+ {
+ generator._returnRequested = false;
+ }
+
var f = _finalizer.Execute(context);
// Clear the pending completion tracking if we completed normally
@@ -118,7 +126,7 @@ private Completion ExecuteCatchResume(EvaluationContext context, Engine engine)
var b = _catch.Execute(context);
// If a generator is suspended (yield), don't run the finally yet
- if (engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
return b;
}
@@ -144,7 +152,7 @@ private Completion ExecuteFinallyResume(EvaluationContext context, Engine engine
var f = _finalizer!.Execute(context);
// If a generator is suspended (yield), don't process the pending completion yet
- if (engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
return f;
}
diff --git a/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs
index c4527698fb..25e7931717 100644
--- a/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintVariableDeclaration.cs
@@ -70,6 +70,14 @@ protected override Completion ExecuteInternal(EvaluationContext context)
if (declaration.Init != null)
{
value = declaration.Init.GetValue(context).Clone();
+
+ // Check for generator suspension after evaluating initializer
+ if (context.IsGeneratorSuspended())
+ {
+ engine._referencePool.Return(lhs);
+ return new Completion(CompletionType.Normal, value, _statement);
+ }
+
if (declaration.Init._expression.IsFunctionDefinition())
{
((Function) value).SetFunctionName(lhs.ReferencedName);
@@ -89,6 +97,12 @@ protected override Completion ExecuteInternal(EvaluationContext context)
var value = declaration.Init.GetValue(context);
+ // Check for generator suspension after evaluating initializer
+ if (context.IsGeneratorSuspended())
+ {
+ return new Completion(CompletionType.Normal, value, _statement);
+ }
+
DestructuringPatternAssignmentExpression.ProcessPatterns(
context,
declaration.LeftPattern,
@@ -109,6 +123,13 @@ protected override Completion ExecuteInternal(EvaluationContext context)
var value = declaration.Init.GetValue(context).Clone();
+ // Check for generator suspension after evaluating initializer
+ if (context.IsGeneratorSuspended())
+ {
+ engine._referencePool.Return(lhs);
+ return new Completion(CompletionType.Normal, value, _statement);
+ }
+
if (declaration.Init._expression.IsFunctionDefinition())
{
((Function) value).SetFunctionName(lhs.ReferencedName);
diff --git a/Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs b/Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs
index f5a3e5599a..88a1486d4a 100644
--- a/Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs
+++ b/Jint/Runtime/Interpreter/Statements/JintWhileStatement.cs
@@ -47,7 +47,7 @@ protected override Completion ExecuteInternal(EvaluationContext context)
}
// Check for generator suspension - if the generator is suspended, we need to exit the loop
- if (context.Engine.ExecutionContext.Suspended)
+ if (context.IsGeneratorSuspended())
{
var generator = context.Engine.ExecutionContext.Generator;
var suspendedValue = generator?._suspendedValue ?? completion.Value;
diff --git a/Jint/Runtime/SuspendData.cs b/Jint/Runtime/SuspendData.cs
new file mode 100644
index 0000000000..c3334f3ce3
--- /dev/null
+++ b/Jint/Runtime/SuspendData.cs
@@ -0,0 +1,65 @@
+using Jint.Native;
+using Jint.Native.Iterator;
+using Jint.Runtime.Environments;
+
+namespace Jint.Runtime;
+
+///
+/// Base class for generator suspension state.
+/// Used to track iterator state when a generator yields inside constructs like for-of loops
+/// or destructuring patterns.
+///
+internal abstract class SuspendData
+{
+ ///
+ /// The iterator instance that was in progress when the generator suspended.
+ ///
+ public IteratorInstance Iterator { get; init; } = null!;
+}
+
+///
+/// Stores the state of an array destructuring pattern when a generator yields inside it.
+/// When a generator yields during array destructuring (e.g., [x[yield]] = iterable),
+/// the iterator must be preserved so it can be properly closed when the generator
+/// completes or returns.
+///
+internal sealed class DestructuringSuspendData : SuspendData
+{
+ ///
+ /// The current element index in the destructuring pattern.
+ ///
+ public uint ElementIndex { get; set; }
+
+ ///
+ /// Whether the iterator has been exhausted (done=true).
+ ///
+ public bool Done { get; set; }
+
+ ///
+ /// Values already retrieved from the iterator for each element position.
+ /// Used when resuming to avoid calling next() again.
+ ///
+ public JsValue[]? RetrievedValues { get; set; }
+}
+
+///
+/// Stores the state of a for-of/for-in loop when a generator yields inside it.
+///
+internal sealed class ForOfSuspendData : SuspendData
+{
+ ///
+ /// The current value being processed (from TryIteratorStep).
+ /// Needed when yield happens during destructuring or body execution.
+ ///
+ public JsValue? CurrentValue { get; set; }
+
+ ///
+ /// The accumulated result value (v) from previous iterations.
+ ///
+ public JsValue AccumulatedValue { get; set; } = JsValue.Undefined;
+
+ ///
+ /// The iteration environment for lexical bindings (let/const in for-of).
+ ///
+ public DeclarativeEnvironment? IterationEnv { get; set; }
+}
diff --git a/Jint/Runtime/YieldSuspendException.cs b/Jint/Runtime/YieldSuspendException.cs
deleted file mode 100644
index f51163abe9..0000000000
--- a/Jint/Runtime/YieldSuspendException.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Jint.Native;
-
-namespace Jint.Runtime;
-
-///
-/// Internal exception used to implement yield suspension.
-/// When a yield expression is evaluated, this exception is thrown to immediately
-/// interrupt expression evaluation and propagate control back up the call stack.
-/// This is caught at the statement level to handle generator suspension properly.
-///
-internal sealed class YieldSuspendException : Exception
-{
- public JsValue YieldedValue { get; }
-
- public YieldSuspendException(JsValue yieldedValue) : base()
- {
- YieldedValue = yieldedValue;
- }
-}