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; - } -}