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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Compilation/AsyncArrowMoveNextEmitter.Statements.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
using System.Reflection.Emit;
using SharpTS.Parsing;

namespace SharpTS.Compilation;

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.
Expand Down
6 changes: 5 additions & 1 deletion Compilation/AsyncGeneratorMoveNextEmitter.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>
Expand Down
313 changes: 3 additions & 310 deletions Compilation/AsyncMoveNextEmitter.Statements.Loops.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Reflection;
using System.Reflection.Emit;
using SharpTS.Parsing;

namespace SharpTS.Compilation;
Expand All @@ -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;
}
Expand All @@ -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<object?>
// (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<object> 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<object?>
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<object>
_il.Emit(OpCodes.Ldloc, asyncGenLocal);
_il.Emit(OpCodes.Callvirt, _ctx.Runtime.AsyncGeneratorNextMethod);

// Await the Task<object>
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<string, object> 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<object> 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
Expand Down
Loading
Loading