diff --git a/Compilation/AsyncArrowMoveNextEmitter.Statements.cs b/Compilation/AsyncArrowMoveNextEmitter.Statements.cs index 474e72c3..e5b6f59c 100644 --- a/Compilation/AsyncArrowMoveNextEmitter.Statements.cs +++ b/Compilation/AsyncArrowMoveNextEmitter.Statements.cs @@ -1,4 +1,5 @@ using System.Reflection.Emit; +using SharpTS.Parsing; namespace SharpTS.Compilation; @@ -6,6 +7,23 @@ public partial class AsyncArrowMoveNextEmitter { // Statement dispatch is now inherited from StatementEmitterBase + protected override void EmitForOf(Stmt.ForOf f) + { + if (f.IsAsync) + { + // for await...of must drive the async-iterator protocol, not a synchronous + // IEnumerable enumeration. Without this override the loop fell through to the + // sync for-of path in StatementEmitterBase and threw InvalidCastException + // casting the async-generator state machine to IEnumerable (#430/#645). + EmitForAwaitOf(f); + return; + } + + // Sync for...of: delegate to base (uses DeclareLoopVariable/EmitStoreLoopVariable + // overrides to handle hoisted state machine fields) + base.EmitForOf(f); + } + // Falling off the end of a block-bodied async arrow completes with `undefined`, not null // (#587). Explicit returns are handled by EmitReturn; this is only the implicit-completion // fall-through emitted after the body. diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.cs index e5e25748..70b0d444 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.cs @@ -168,7 +168,11 @@ private void EmitForOfWithHoistedEnumerator(Stmt.ForOf f, FieldBuilder enumerato ExitLoop(); } - private void EmitForAwaitOf(Stmt.ForOf f) + // Overrides the shared StatementEmitterBase.EmitForAwaitOf: an async generator that + // consumes another async iterable needs its own error-propagation handling (the + // try/catch around GetResult below) and a strict done check, so it keeps a specialized + // lowering rather than the shared one used by async functions/arrows (#430/#645). + protected override void EmitForAwaitOf(Stmt.ForOf f) { // for await...of iterates over async iterables // We use the $IAsyncGenerator.next() method which returns Task diff --git a/Compilation/AsyncMoveNextEmitter.Statements.Loops.cs b/Compilation/AsyncMoveNextEmitter.Statements.Loops.cs index 1f059eb2..52927c95 100644 --- a/Compilation/AsyncMoveNextEmitter.Statements.Loops.cs +++ b/Compilation/AsyncMoveNextEmitter.Statements.Loops.cs @@ -1,5 +1,3 @@ -using System.Reflection; -using System.Reflection.Emit; using SharpTS.Parsing; namespace SharpTS.Compilation; @@ -12,6 +10,7 @@ protected override void EmitForOf(Stmt.ForOf f) { if (f.IsAsync) { + // for await...of uses the shared async-iterator lowering in StatementEmitterBase. EmitForAwaitOf(f); return; } @@ -21,314 +20,8 @@ protected override void EmitForOf(Stmt.ForOf f) base.EmitForOf(f); } - private void EmitForAwaitOf(Stmt.ForOf f) - { - // for await...of iterates over async iterables - // First try Symbol.asyncIterator protocol, then fall back to $IAsyncGenerator - // The result from next() is a promise/task with { value, done } properties - - string varName = f.Variable.Lexeme; - var varField = _builder.GetVariableField(varName); - - // Emit the async iterable expression - EmitExpression(f.Iterable); - EnsureBoxed(); - - // Store the iterable - var iterableLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, iterableLocal); - - // Try async iterator protocol: GetIteratorFunction(iterable, Symbol.asyncIterator) - var asyncIteratorFnLocal = _il.DeclareLocal(_types.Object); - var asyncGenLabel = _il.DefineLabel(); - var afterLoopLabel = _il.DefineLabel(); - - _il.Emit(OpCodes.Ldloc, iterableLocal); - _il.Emit(OpCodes.Ldsfld, _ctx!.Runtime!.SymbolAsyncIterator); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetIteratorFunction); - _il.Emit(OpCodes.Stloc, asyncIteratorFnLocal); - - // If async iterator function is null, fall back to $IAsyncGenerator - _il.Emit(OpCodes.Ldloc, asyncIteratorFnLocal); - _il.Emit(OpCodes.Brfalse, asyncGenLabel); - - // ===== Custom async iterator protocol path ===== - { - // Call the async iterator function to get the async iterator object - // Use InvokeMethodValue to properly bind 'this' to the iterable object - _il.Emit(OpCodes.Ldloc, iterableLocal); // receiver (this) - _il.Emit(OpCodes.Ldloc, asyncIteratorFnLocal); // method - _il.Emit(OpCodes.Ldc_I4_0); - _il.Emit(OpCodes.Newarr, _types.Object); // args - _il.Emit(OpCodes.Call, _ctx.Runtime.InvokeMethodValue); - - var asyncIteratorLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, asyncIteratorLocal); - - var startLabel = _il.DefineLabel(); - var endLabel = _il.DefineLabel(); - var cleanupLabel = _il.DefineLabel(); - var continueLabel = _il.DefineLabel(); - - // Break goes to cleanup (calls iterator.return()), not directly to end - EnterLoop(cleanupLabel, continueLabel); - - _il.MarkLabel(startLabel); - - // Call InvokeIteratorNext(asyncIterator) which returns a Promise/Task - _il.Emit(OpCodes.Ldloc, asyncIteratorLocal); - _il.Emit(OpCodes.Call, _ctx.Runtime.InvokeIteratorNext); - - // The result should be a Task/Promise - await it - // Store as object first, then check if it's a $TSPromise or Task - var nextResultLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, nextResultLocal); - - // If it's a $TSPromise, unwrap to its inner Task - // (custom async iterators may return $TSPromise via WrapTaskAsPromise) - var notTSPromiseLabel = _il.DefineLabel(); - _il.Emit(OpCodes.Ldloc, nextResultLocal); - _il.Emit(OpCodes.Isinst, _ctx.Runtime.TSPromiseType); - _il.Emit(OpCodes.Brfalse, notTSPromiseLabel); - // Replace nextResultLocal with the inner Task - _il.Emit(OpCodes.Ldloc, nextResultLocal); - _il.Emit(OpCodes.Castclass, _ctx.Runtime.TSPromiseType); - _il.Emit(OpCodes.Callvirt, _ctx.Runtime.TSPromiseTaskGetter); - _il.Emit(OpCodes.Stloc, nextResultLocal); - _il.MarkLabel(notTSPromiseLabel); - - // Check if result is a Task and await it - var isTaskLabel = _il.DefineLabel(); - var afterAwaitLabel = _il.DefineLabel(); - var resultLocal = _il.DeclareLocal(_types.Object); - - _il.Emit(OpCodes.Ldloc, nextResultLocal); - _il.Emit(OpCodes.Isinst, _types.TaskOfObject); - _il.Emit(OpCodes.Brtrue, isTaskLabel); - - // Not a task - use the result directly (might be a sync iterator result) - _il.Emit(OpCodes.Ldloc, nextResultLocal); - _il.Emit(OpCodes.Stloc, resultLocal); - _il.Emit(OpCodes.Br, afterAwaitLabel); - - // Is a Task - await it - _il.MarkLabel(isTaskLabel); - _il.Emit(OpCodes.Ldloc, nextResultLocal); - _il.Emit(OpCodes.Castclass, _types.TaskOfObject); - var taskLocal = _il.DeclareLocal(_types.TaskOfObject); - _il.Emit(OpCodes.Stloc, taskLocal); - _il.Emit(OpCodes.Ldloc, taskLocal); - var getAwaiter = _types.GetMethodNoParams(_types.TaskOfObject, "GetAwaiter"); - _il.Emit(OpCodes.Call, getAwaiter); - var awaiterLocal = _il.DeclareLocal(_types.TaskAwaiterOfObject); - _il.Emit(OpCodes.Stloc, awaiterLocal); - - _il.Emit(OpCodes.Ldloca, awaiterLocal); - var getResult = _types.GetMethodNoParams(_types.TaskAwaiterOfObject, "GetResult"); - _il.Emit(OpCodes.Call, getResult); - _il.Emit(OpCodes.Stloc, resultLocal); - - _il.MarkLabel(afterAwaitLabel); - - // Check if done: use GetIteratorDone - _il.Emit(OpCodes.Ldloc, resultLocal); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetIteratorDone); - _il.Emit(OpCodes.Brtrue, endLabel); - - // Get value: use GetIteratorValue - _il.Emit(OpCodes.Ldloc, resultLocal); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetIteratorValue); - - // Assign to loop variable - if (varField != null) - { - var valueTemp = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, valueTemp); - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldloc, valueTemp); - _il.Emit(OpCodes.Stfld, varField); - } - else - { - var varLocal = _il.DeclareLocal(_types.Object); - _ctx.Locals.RegisterLocal(varName, varLocal); - _il.Emit(OpCodes.Stloc, varLocal); - } - - EmitStatement(f.Body); - - _il.MarkLabel(continueLabel); - _il.Emit(OpCodes.Br, startLabel); - - // Cleanup on break: call iterator.return() to trigger finally blocks in generators - _il.MarkLabel(cleanupLabel); - { - // Get the "return" method from the async iterator - _il.Emit(OpCodes.Ldloc, asyncIteratorLocal); - _il.Emit(OpCodes.Ldstr, "return"); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetProperty); - - var returnFnLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, returnFnLocal); - - // If no return method, skip cleanup — iterator.return() is - // optional per the iterator protocol. GetProperty reports a - // missing member as either null or $Undefined, and - // InvokeMethodValue now throws TypeError for both (#260), so - // guard against both here. - _il.Emit(OpCodes.Ldloc, returnFnLocal); - _il.Emit(OpCodes.Brfalse, endLabel); - _il.Emit(OpCodes.Ldloc, returnFnLocal); - _il.Emit(OpCodes.Isinst, _ctx.Runtime.UndefinedType); - _il.Emit(OpCodes.Brtrue, endLabel); - - // Call: InvokeMethodValue(asyncIterator, returnFn, []) - _il.Emit(OpCodes.Ldloc, asyncIteratorLocal); - _il.Emit(OpCodes.Ldloc, returnFnLocal); - _il.Emit(OpCodes.Ldc_I4_0); - _il.Emit(OpCodes.Newarr, _types.Object); - _il.Emit(OpCodes.Call, _ctx.Runtime.InvokeMethodValue); - - // If result is a Task, await it. Unwrap $TSPromise first if present. - var returnResultLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, returnResultLocal); - - // If $TSPromise, replace with inner Task - var returnNotTSPromiseLabel = _il.DefineLabel(); - _il.Emit(OpCodes.Ldloc, returnResultLocal); - _il.Emit(OpCodes.Isinst, _ctx.Runtime.TSPromiseType); - _il.Emit(OpCodes.Brfalse, returnNotTSPromiseLabel); - _il.Emit(OpCodes.Ldloc, returnResultLocal); - _il.Emit(OpCodes.Castclass, _ctx.Runtime.TSPromiseType); - _il.Emit(OpCodes.Callvirt, _ctx.Runtime.TSPromiseTaskGetter); - _il.Emit(OpCodes.Stloc, returnResultLocal); - _il.MarkLabel(returnNotTSPromiseLabel); - - _il.Emit(OpCodes.Ldloc, returnResultLocal); - _il.Emit(OpCodes.Isinst, _types.TaskOfObject); - _il.Emit(OpCodes.Brfalse, endLabel); - - _il.Emit(OpCodes.Ldloc, returnResultLocal); - _il.Emit(OpCodes.Castclass, _types.TaskOfObject); - var cleanupTaskLocal = _il.DeclareLocal(_types.TaskOfObject); - _il.Emit(OpCodes.Stloc, cleanupTaskLocal); - _il.Emit(OpCodes.Ldloc, cleanupTaskLocal); - var cleanupGetAwaiter = _types.GetMethodNoParams(_types.TaskOfObject, "GetAwaiter"); - _il.Emit(OpCodes.Call, cleanupGetAwaiter); - var cleanupAwaiterLocal = _il.DeclareLocal(_types.TaskAwaiterOfObject); - _il.Emit(OpCodes.Stloc, cleanupAwaiterLocal); - _il.Emit(OpCodes.Ldloca, cleanupAwaiterLocal); - var cleanupGetResult = _types.GetMethodNoParams(_types.TaskAwaiterOfObject, "GetResult"); - _il.Emit(OpCodes.Call, cleanupGetResult); - _il.Emit(OpCodes.Pop); // Discard return result - } - - _il.MarkLabel(endLabel); - ExitLoop(); - _il.Emit(OpCodes.Br, afterLoopLabel); // Skip the fallback path - } - - // ===== $IAsyncGenerator fallback path ===== - _il.MarkLabel(asyncGenLabel); - { - // Cast to $IAsyncGenerator interface - var asyncGenInterface = _ctx.Runtime.AsyncGeneratorInterfaceType; - _il.Emit(OpCodes.Ldloc, iterableLocal); - _il.Emit(OpCodes.Castclass, asyncGenInterface); - - // Store the async generator in a local - var asyncGenLocal = _il.DeclareLocal(asyncGenInterface); - _il.Emit(OpCodes.Stloc, asyncGenLocal); - - var genStartLabel = _il.DefineLabel(); - var genEndLabel = _il.DefineLabel(); - var genCleanupLabel = _il.DefineLabel(); - var genContinueLabel = _il.DefineLabel(); - - // Break goes to cleanup (calls generator.return()), not directly to end - EnterLoop(genCleanupLabel, genContinueLabel); - - _il.MarkLabel(genStartLabel); - - // Call next() which returns Task - _il.Emit(OpCodes.Ldloc, asyncGenLocal); - _il.Emit(OpCodes.Callvirt, _ctx.Runtime.AsyncGeneratorNextMethod); - - // Await the Task - var genTaskLocal = _il.DeclareLocal(_types.TaskOfObject); - _il.Emit(OpCodes.Stloc, genTaskLocal); - _il.Emit(OpCodes.Ldloc, genTaskLocal); - var genGetAwaiter = _types.GetMethodNoParams(_types.TaskOfObject, "GetAwaiter"); - _il.Emit(OpCodes.Call, genGetAwaiter); - var genAwaiterLocal = _il.DeclareLocal(_types.TaskAwaiterOfObject); - _il.Emit(OpCodes.Stloc, genAwaiterLocal); - _il.Emit(OpCodes.Ldloca, genAwaiterLocal); - var genGetResult = _types.GetMethodNoParams(_types.TaskAwaiterOfObject, "GetResult"); - _il.Emit(OpCodes.Call, genGetResult); - - // Result is a Dictionary with { value, done } - var genResultLocal = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, genResultLocal); - - // Check if done: GetProperty(result, "done") - _il.Emit(OpCodes.Ldloc, genResultLocal); - _il.Emit(OpCodes.Ldstr, "done"); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetProperty); - - // Convert to bool and check - natural done exits directly (no cleanup needed) - _il.Emit(OpCodes.Call, _ctx.Runtime.IsTruthy); - _il.Emit(OpCodes.Brtrue, genEndLabel); - - // Get value: GetProperty(result, "value") - _il.Emit(OpCodes.Ldloc, genResultLocal); - _il.Emit(OpCodes.Ldstr, "value"); - _il.Emit(OpCodes.Call, _ctx.Runtime.GetProperty); - - // Assign to loop variable - if (varField != null) - { - var genValueTemp = _il.DeclareLocal(_types.Object); - _il.Emit(OpCodes.Stloc, genValueTemp); - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldloc, genValueTemp); - _il.Emit(OpCodes.Stfld, varField); - } - else - { - var genVarLocal = _il.DeclareLocal(_types.Object); - _ctx.Locals.RegisterLocal(varName, genVarLocal); - _il.Emit(OpCodes.Stloc, genVarLocal); - } - - EmitStatement(f.Body); - - _il.MarkLabel(genContinueLabel); - _il.Emit(OpCodes.Br, genStartLabel); - - // Cleanup on break: call generator.return(null) to trigger finally blocks - _il.MarkLabel(genCleanupLabel); - _il.Emit(OpCodes.Ldloc, asyncGenLocal); - _il.Emit(OpCodes.Ldnull); - _il.Emit(OpCodes.Callvirt, _ctx.Runtime.AsyncGeneratorReturnMethod); - // Await the Task result and discard it - var cleanupTaskLocal = _il.DeclareLocal(_types.TaskOfObject); - _il.Emit(OpCodes.Stloc, cleanupTaskLocal); - _il.Emit(OpCodes.Ldloc, cleanupTaskLocal); - _il.Emit(OpCodes.Call, genGetAwaiter); - var cleanupAwaiterLocal = _il.DeclareLocal(_types.TaskAwaiterOfObject); - _il.Emit(OpCodes.Stloc, cleanupAwaiterLocal); - _il.Emit(OpCodes.Ldloca, cleanupAwaiterLocal); - _il.Emit(OpCodes.Call, genGetResult); - _il.Emit(OpCodes.Pop); - _il.Emit(OpCodes.Br, genEndLabel); - - _il.MarkLabel(genEndLabel); - ExitLoop(); - } - - // Common exit point for both paths - _il.MarkLabel(afterLoopLabel); - } + // EmitForAwaitOf: inherited from StatementEmitterBase. The async-function, async-arrow, + // and async-generator emitters now share one async-iterator lowering (#430/#645). // EmitDoWhile: inherited from StatementEmitterBase (identical logic) // EmitForIn: inherited from StatementEmitterBase (uses DeclareLoopVariable/EmitStoreLoopVariable diff --git a/Compilation/StatementEmitterBase.cs b/Compilation/StatementEmitterBase.cs index 767907fb..5f2578f0 100644 --- a/Compilation/StatementEmitterBase.cs +++ b/Compilation/StatementEmitterBase.cs @@ -516,6 +516,316 @@ protected virtual void EmitForOf(Stmt.ForOf f) ExitLoop(); } + /// + /// Emits a for await...of loop over an async iterable. Shared by every async + /// state-machine emitter (async functions, async arrows, async generators) so the + /// async-iterator lowering lives in exactly one place instead of being duplicated and + /// drifting per emitter. + /// + /// + /// First tries the Symbol.asyncIterator protocol (custom async iterators), then + /// falls back to the $IAsyncGenerator interface. Each next() result is + /// awaited via a blocking GetAwaiter().GetResult(); the loop does not suspend the + /// enclosing state machine, so loop-scoped IL locals are safe as long as the loop body + /// itself does not await across them. + /// + /// Subclasses dispatch here from their override when + /// is set. supplies + /// the loop-variable storage so each emitter routes the binding through its own state + /// machine fields. + /// + protected virtual void EmitForAwaitOf(Stmt.ForOf f) + { + // for await...of iterates over async iterables. + // First try the Symbol.asyncIterator protocol, then fall back to $IAsyncGenerator. + // The result from next() is a promise/task with { value, done } properties. + var il = IL; + var types = Types; + var runtime = Ctx.Runtime!; + + string varName = f.Variable.Lexeme; + + // Emit the async iterable expression + EmitExpression(f.Iterable); + EnsureBoxed(); + + // Store the iterable + var iterableLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, iterableLocal); + + // Declare the loop variable AFTER the iterable is evaluated (so the iterable expression + // can't resolve to the not-yet-bound loop variable). Storage is routed through the + // DeclareLoopVariable/EmitStoreLoopVariable hooks so each emitter binds it correctly: a + // hoisted state-machine field for async functions and async generators, or an + // emitter-local for async arrows (whose resolver reads its own local map, not Ctx.Locals). + var loopVarLocal = DeclareLoopVariable(varName); + + // Try async iterator protocol: GetIteratorFunction(iterable, Symbol.asyncIterator) + var asyncIteratorFnLocal = il.DeclareLocal(types.Object); + var asyncGenLabel = il.DefineLabel(); + var afterLoopLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldloc, iterableLocal); + il.Emit(OpCodes.Ldsfld, runtime.SymbolAsyncIterator); + il.Emit(OpCodes.Call, runtime.GetIteratorFunction); + il.Emit(OpCodes.Stloc, asyncIteratorFnLocal); + + // If async iterator function is null, fall back to $IAsyncGenerator + il.Emit(OpCodes.Ldloc, asyncIteratorFnLocal); + il.Emit(OpCodes.Brfalse, asyncGenLabel); + + // ===== Custom async iterator protocol path ===== + { + // Call the async iterator function to get the async iterator object. + // Use InvokeMethodValue to properly bind 'this' to the iterable object. + il.Emit(OpCodes.Ldloc, iterableLocal); // receiver (this) + il.Emit(OpCodes.Ldloc, asyncIteratorFnLocal); // method + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, types.Object); // args + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + + var asyncIteratorLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, asyncIteratorLocal); + + var startLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + var cleanupLabel = il.DefineLabel(); + var continueLabel = il.DefineLabel(); + + // Break goes to cleanup (calls iterator.return()), not directly to end + EnterLoop(cleanupLabel, continueLabel); + + il.MarkLabel(startLabel); + + // Call InvokeIteratorNext(asyncIterator) which returns a Promise/Task + il.Emit(OpCodes.Ldloc, asyncIteratorLocal); + il.Emit(OpCodes.Call, runtime.InvokeIteratorNext); + + // The result should be a Task/Promise - await it. + // Store as object first, then check if it's a $TSPromise or Task. + var nextResultLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, nextResultLocal); + + // If it's a $TSPromise, unwrap to its inner Task + // (custom async iterators may return $TSPromise via WrapTaskAsPromise) + var notTSPromiseLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, nextResultLocal); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brfalse, notTSPromiseLabel); + // Replace nextResultLocal with the inner Task + il.Emit(OpCodes.Ldloc, nextResultLocal); + il.Emit(OpCodes.Castclass, runtime.TSPromiseType); + il.Emit(OpCodes.Callvirt, runtime.TSPromiseTaskGetter); + il.Emit(OpCodes.Stloc, nextResultLocal); + il.MarkLabel(notTSPromiseLabel); + + // Check if result is a Task and await it + var isTaskLabel = il.DefineLabel(); + var afterAwaitLabel = il.DefineLabel(); + var resultLocal = il.DeclareLocal(types.Object); + + il.Emit(OpCodes.Ldloc, nextResultLocal); + il.Emit(OpCodes.Isinst, types.TaskOfObject); + il.Emit(OpCodes.Brtrue, isTaskLabel); + + // Not a task - use the result directly (might be a sync iterator result) + il.Emit(OpCodes.Ldloc, nextResultLocal); + il.Emit(OpCodes.Stloc, resultLocal); + il.Emit(OpCodes.Br, afterAwaitLabel); + + // Is a Task - await it + il.MarkLabel(isTaskLabel); + il.Emit(OpCodes.Ldloc, nextResultLocal); + il.Emit(OpCodes.Castclass, types.TaskOfObject); + var taskLocal = il.DeclareLocal(types.TaskOfObject); + il.Emit(OpCodes.Stloc, taskLocal); + il.Emit(OpCodes.Ldloc, taskLocal); + var getAwaiter = types.GetMethodNoParams(types.TaskOfObject, "GetAwaiter"); + il.Emit(OpCodes.Call, getAwaiter); + var awaiterLocal = il.DeclareLocal(types.TaskAwaiterOfObject); + il.Emit(OpCodes.Stloc, awaiterLocal); + + il.Emit(OpCodes.Ldloca, awaiterLocal); + var getResult = types.GetMethodNoParams(types.TaskAwaiterOfObject, "GetResult"); + il.Emit(OpCodes.Call, getResult); + il.Emit(OpCodes.Stloc, resultLocal); + + il.MarkLabel(afterAwaitLabel); + + // Check if done: use GetIteratorDone + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Call, runtime.GetIteratorDone); + il.Emit(OpCodes.Brtrue, endLabel); + + // Assign to loop variable (value via GetIteratorValue) + EmitStoreLoopVariable(loopVarLocal, varName, () => + { + il.Emit(OpCodes.Ldloc, resultLocal); + il.Emit(OpCodes.Call, runtime.GetIteratorValue); + }); + + EmitStatement(f.Body); + + il.MarkLabel(continueLabel); + il.Emit(OpCodes.Br, startLabel); + + // Cleanup on break: call iterator.return() to trigger finally blocks in generators + il.MarkLabel(cleanupLabel); + { + // Get the "return" method from the async iterator + il.Emit(OpCodes.Ldloc, asyncIteratorLocal); + il.Emit(OpCodes.Ldstr, "return"); + il.Emit(OpCodes.Call, runtime.GetProperty); + + var returnFnLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, returnFnLocal); + + // If no return method, skip cleanup — iterator.return() is + // optional per the iterator protocol. GetProperty reports a + // missing member as either null or $Undefined, and + // InvokeMethodValue now throws TypeError for both (#260), so + // guard against both here. + il.Emit(OpCodes.Ldloc, returnFnLocal); + il.Emit(OpCodes.Brfalse, endLabel); + il.Emit(OpCodes.Ldloc, returnFnLocal); + il.Emit(OpCodes.Isinst, runtime.UndefinedType); + il.Emit(OpCodes.Brtrue, endLabel); + + // Call: InvokeMethodValue(asyncIterator, returnFn, []) + il.Emit(OpCodes.Ldloc, asyncIteratorLocal); + il.Emit(OpCodes.Ldloc, returnFnLocal); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, types.Object); + il.Emit(OpCodes.Call, runtime.InvokeMethodValue); + + // If result is a Task, await it. Unwrap $TSPromise first if present. + var returnResultLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, returnResultLocal); + + // If $TSPromise, replace with inner Task + var returnNotTSPromiseLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, returnResultLocal); + il.Emit(OpCodes.Isinst, runtime.TSPromiseType); + il.Emit(OpCodes.Brfalse, returnNotTSPromiseLabel); + il.Emit(OpCodes.Ldloc, returnResultLocal); + il.Emit(OpCodes.Castclass, runtime.TSPromiseType); + il.Emit(OpCodes.Callvirt, runtime.TSPromiseTaskGetter); + il.Emit(OpCodes.Stloc, returnResultLocal); + il.MarkLabel(returnNotTSPromiseLabel); + + il.Emit(OpCodes.Ldloc, returnResultLocal); + il.Emit(OpCodes.Isinst, types.TaskOfObject); + il.Emit(OpCodes.Brfalse, endLabel); + + il.Emit(OpCodes.Ldloc, returnResultLocal); + il.Emit(OpCodes.Castclass, types.TaskOfObject); + var cleanupTaskLocal = il.DeclareLocal(types.TaskOfObject); + il.Emit(OpCodes.Stloc, cleanupTaskLocal); + il.Emit(OpCodes.Ldloc, cleanupTaskLocal); + var cleanupGetAwaiter = types.GetMethodNoParams(types.TaskOfObject, "GetAwaiter"); + il.Emit(OpCodes.Call, cleanupGetAwaiter); + var cleanupAwaiterLocal = il.DeclareLocal(types.TaskAwaiterOfObject); + il.Emit(OpCodes.Stloc, cleanupAwaiterLocal); + il.Emit(OpCodes.Ldloca, cleanupAwaiterLocal); + var cleanupGetResult = types.GetMethodNoParams(types.TaskAwaiterOfObject, "GetResult"); + il.Emit(OpCodes.Call, cleanupGetResult); + il.Emit(OpCodes.Pop); // Discard return result + } + + il.MarkLabel(endLabel); + ExitLoop(); + il.Emit(OpCodes.Br, afterLoopLabel); // Skip the fallback path + } + + // ===== $IAsyncGenerator fallback path ===== + il.MarkLabel(asyncGenLabel); + { + // Cast to $IAsyncGenerator interface + var asyncGenInterface = runtime.AsyncGeneratorInterfaceType; + il.Emit(OpCodes.Ldloc, iterableLocal); + il.Emit(OpCodes.Castclass, asyncGenInterface); + + // Store the async generator in a local + var asyncGenLocal = il.DeclareLocal(asyncGenInterface); + il.Emit(OpCodes.Stloc, asyncGenLocal); + + var genStartLabel = il.DefineLabel(); + var genEndLabel = il.DefineLabel(); + var genCleanupLabel = il.DefineLabel(); + var genContinueLabel = il.DefineLabel(); + + // Break goes to cleanup (calls generator.return()), not directly to end + EnterLoop(genCleanupLabel, genContinueLabel); + + il.MarkLabel(genStartLabel); + + // Call next() which returns Task + il.Emit(OpCodes.Ldloc, asyncGenLocal); + il.Emit(OpCodes.Callvirt, runtime.AsyncGeneratorNextMethod); + + // Await the Task + var genTaskLocal = il.DeclareLocal(types.TaskOfObject); + il.Emit(OpCodes.Stloc, genTaskLocal); + il.Emit(OpCodes.Ldloc, genTaskLocal); + var genGetAwaiter = types.GetMethodNoParams(types.TaskOfObject, "GetAwaiter"); + il.Emit(OpCodes.Call, genGetAwaiter); + var genAwaiterLocal = il.DeclareLocal(types.TaskAwaiterOfObject); + il.Emit(OpCodes.Stloc, genAwaiterLocal); + il.Emit(OpCodes.Ldloca, genAwaiterLocal); + var genGetResult = types.GetMethodNoParams(types.TaskAwaiterOfObject, "GetResult"); + il.Emit(OpCodes.Call, genGetResult); + + // Result is a Dictionary with { value, done } + var genResultLocal = il.DeclareLocal(types.Object); + il.Emit(OpCodes.Stloc, genResultLocal); + + // Check if done: GetProperty(result, "done") + il.Emit(OpCodes.Ldloc, genResultLocal); + il.Emit(OpCodes.Ldstr, "done"); + il.Emit(OpCodes.Call, runtime.GetProperty); + + // Convert to bool and check - natural done exits directly (no cleanup needed) + il.Emit(OpCodes.Call, runtime.IsTruthy); + il.Emit(OpCodes.Brtrue, genEndLabel); + + // Assign to loop variable (value via GetProperty(result, "value")) + EmitStoreLoopVariable(loopVarLocal, varName, () => + { + il.Emit(OpCodes.Ldloc, genResultLocal); + il.Emit(OpCodes.Ldstr, "value"); + il.Emit(OpCodes.Call, runtime.GetProperty); + }); + + EmitStatement(f.Body); + + il.MarkLabel(genContinueLabel); + il.Emit(OpCodes.Br, genStartLabel); + + // Cleanup on break: call generator.return(null) to trigger finally blocks + il.MarkLabel(genCleanupLabel); + il.Emit(OpCodes.Ldloc, asyncGenLocal); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Callvirt, runtime.AsyncGeneratorReturnMethod); + // Await the Task result and discard it + var cleanupGenTaskLocal = il.DeclareLocal(types.TaskOfObject); + il.Emit(OpCodes.Stloc, cleanupGenTaskLocal); + il.Emit(OpCodes.Ldloc, cleanupGenTaskLocal); + il.Emit(OpCodes.Call, genGetAwaiter); + var cleanupGenAwaiterLocal = il.DeclareLocal(types.TaskAwaiterOfObject); + il.Emit(OpCodes.Stloc, cleanupGenAwaiterLocal); + il.Emit(OpCodes.Ldloca, cleanupGenAwaiterLocal); + il.Emit(OpCodes.Call, genGetResult); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Br, genEndLabel); + + il.MarkLabel(genEndLabel); + ExitLoop(); + } + + // Common exit point for both paths + il.MarkLabel(afterLoopLabel); + } + /// /// Emits a for...in loop iterating over object keys. /// diff --git a/SharpTS.Tests/Compilation/EmitterSyncTests.cs b/SharpTS.Tests/Compilation/EmitterSyncTests.cs index 20367b40..2b06bc42 100644 --- a/SharpTS.Tests/Compilation/EmitterSyncTests.cs +++ b/SharpTS.Tests/Compilation/EmitterSyncTests.cs @@ -79,6 +79,7 @@ public class EmitterSyncTests // --- Genuinely different behavior --- "EmitReturn", // Async arrow return: store result + SetResult "EmitTryCatch", // Await-aware exception handling + "EmitForOf", // #430/#645: for-await-of dispatch to the shared async-iterator lowering (sync for-of delegates to base) "EmitVarDeclaration", // Capture indirection for outer variables "EmitVariable", // Capture indirection "EmitAssign", // Capture indirection @@ -129,6 +130,7 @@ public class EmitterSyncTests "EmitReturn", // Async generator return: store in CurrentField + state -2 "EmitTryCatch", // Suspension-aware exception handling (flag-based) "EmitForOf", // for-await-of + hoisted enumerator for yield/await in loops + "EmitForAwaitOf", // #430/#645: async-generator consumer keeps its own error-propagating lowering instead of the shared base one "EmitBranchToLabel", // Leave instead of Br in exception blocks "EmitYield", // Core: yield value + suspend "EmitAwait", // Core: suspend/resume state machine diff --git a/SharpTS.Tests/SharedTests/AsyncArrowFunctionTests.cs b/SharpTS.Tests/SharedTests/AsyncArrowFunctionTests.cs index 2ab1b7fe..743a3555 100644 --- a/SharpTS.Tests/SharedTests/AsyncArrowFunctionTests.cs +++ b/SharpTS.Tests/SharedTests/AsyncArrowFunctionTests.cs @@ -533,4 +533,98 @@ public void AsyncArrow_NestsParameterizedAsyncArrow(ExecutionMode mode) var output = TestHarness.Run(source, mode); Assert.Equal("6 42\n", output); } + + // #430/#645: a `for await...of` over an async generator inside an async ARROW must drive the + // async-iterator protocol. Previously the arrow emitter had no EmitForOf override, so the loop + // fell through to the synchronous for-of path and threw InvalidCastException casting the + // async-generator state machine to IEnumerable in compiled mode. + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncArrow_ForAwaitOfAsyncGenerator(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; } + const run = async () => { + let s = ""; + for await (const v of g()) s += v + ","; + console.log("arrow=" + s); + }; + run(); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("arrow=1,2,\n", output); + } + + // #430/#645: breaking out of a `for await` inside an async arrow must run the loop's cleanup + // (iterator.return()) path without corrupting the loop variable binding. + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncArrow_ForAwaitBreakRunsCleanup(ExecutionMode mode) + { + var source = """ + async function* g() { yield 10; yield 20; yield 30; } + const run = async () => { + for await (const v of g()) { + console.log("v=" + v); + if (v === 20) break; + } + console.log("done"); + }; + run(); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("v=10\nv=20\ndone\n", output); + } + + // #430/#645: the async-arrow `for await` must also honor the custom Symbol.asyncIterator + // protocol (not only the $IAsyncGenerator fast path). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncArrow_ForAwaitCustomAsyncIterator(ExecutionMode mode) + { + var source = """ + const obj = { + [Symbol.asyncIterator]() { + let i = 0; + return { + next() { + return Promise.resolve(i < 3 ? { value: i++, done: false } : { value: undefined, done: true }); + } + }; + } + }; + const run = async () => { + let s = ""; + for await (const v of obj) s += v; + console.log("got=" + s); + }; + run(); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("got=012\n", output); + } + + // #430/#645: the loop variable bound inside an async-arrow `for await` must resolve to the + // arrow's own local store. The original fix landed `null` values here because the binding was + // registered in Ctx.Locals while the arrow's resolver reads its private local map. + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncArrow_ForAwaitAccumulatesCapturedVariable(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; yield 3; } + const run = async () => { + let sum = 0; + for await (const v of g()) sum += v; + console.log("sum=" + sum); + }; + run(); + """; + + var output = TestHarness.Run(source, mode); + Assert.Equal("sum=6\n", output); + } }