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
9 changes: 5 additions & 4 deletions Compilation/ILCompiler.Generators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ private void EmitGeneratorFunctionDCInit(
Stmt.Function funcStmt,
string qualifiedName,
int paramOffset,
Type[]? paramTypes = null)
System.Reflection.ParameterInfo[]? paramTypes = null)
{
if (functionDCField == null ||
!_closures.FunctionDisplayClassCtors.TryGetValue(qualifiedName, out var dcCtor))
Expand All @@ -302,9 +302,10 @@ private void EmitGeneratorFunctionDCInit(
il.Emit(OpCodes.Ldarg, i + paramOffset); // [sm, dc, arg]
// The DC field is object-typed; box a value-type parameter. Free-function stubs pass
// null here (their params are already object slots); instance-method stubs pass the
// resolved typed-parameter array so value types are boxed before the store (#724).
if (paramTypes != null && i < paramTypes.Length && paramTypes[i].IsValueType)
il.Emit(OpCodes.Box, paramTypes[i]);
// method's actual IL parameters (methodBuilder.GetParameters()) so value types are boxed
// before the store (#724) — and a private method's all-`object` slots are left unboxed.
if (paramTypes != null && i < paramTypes.Length && paramTypes[i].ParameterType.IsValueType)
il.Emit(OpCodes.Box, paramTypes[i].ParameterType);
il.Emit(OpCodes.Stfld, dcField); // [sm]
}
}
Expand Down
56 changes: 54 additions & 2 deletions Execution/Interpreter.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ public partial class Interpreter
internal async Task<object?> AwaitPreservingEnvironment(Task<object?> task)
{
var saved = _environment;
// Also preserve the active async generator: a generator body whose yielded expression awaits
// must resume with its own generator (and scope) restored, even if interleaved event-loop work
// ran another generator in the meantime (#752).
var savedGen = CurrentAsyncGenerator;
try
{
return await task;
}
finally
{
_environment = saved;
CurrentAsyncGenerator = savedGen;
}
}

Expand Down Expand Up @@ -261,9 +266,22 @@ private async Task<ExecutionResult> IterateAsyncIterator(object asyncIterator, S

var result = await ExecuteLoopBodyAsync(forOf.Variable.Lexeme, value, forOf.Body);
var (shouldBreak, shouldContinue, abruptResult) = HandleLoopResult(result, labels);
if (shouldBreak) return ExecutionResult.Success();
// An early exit (break, or a return/throw out of the loop body) closes the iterator before
// leaving: ECMA-262 AsyncIteratorClose calls return() and awaits it, so a suspended async
// generator runs its finally blocks (#697 / cleanup). A lazy generator is otherwise simply
// abandoned at its yield and its finally never runs. The labels (#728) match a labeled
// break/continue that targets this for-await loop rather than escaping it.
if (shouldBreak)
{
await CloseAsyncIteratorOnEarlyExit(asyncIterator);
return ExecutionResult.Success();
}
if (shouldContinue) continue;
if (abruptResult.HasValue) return abruptResult.Value;
if (abruptResult.HasValue)
{
await CloseAsyncIteratorOnEarlyExit(asyncIterator);
return abruptResult.Value;
}

// Process any pending timer callbacks
ProcessPendingCallbacks();
Expand All @@ -272,6 +290,40 @@ private async Task<ExecutionResult> IterateAsyncIterator(object asyncIterator, S
return ExecutionResult.Success();
}

/// <summary>
/// Closes an async iterator when a <c>for await…of</c> exits early (break, or a return/throw out of
/// the loop body) — ECMA-262 AsyncIteratorClose. Calls <c>return()</c> if the iterator provides one
/// and awaits it, so a suspended async generator runs its <c>finally</c> blocks before the loop
/// leaves. Cleanup is best-effort: a missing <c>return()</c> is skipped, and a rejection from the
/// <c>return()</c> itself is swallowed so the loop's own completion (the break / return / throw that
/// triggered the close) takes precedence.
/// </summary>
private async Task CloseAsyncIteratorOnEarlyExit(object asyncIterator)
{
object? result;
try
{
result = CallMethodOnObject(asyncIterator, "return", []);
}
catch
{
// No return() method (or it threw synchronously) — nothing to close.
return;
}

try
{
if (result is SharpTSPromise promise)
await AwaitPreservingEnvironment(promise.Task);
else if (result is Task<object?> task)
await AwaitPreservingEnvironment(task);
}
catch
{
// The iterator's return() rejected during cleanup; the loop's own completion wins.
}
}

/// <summary>
/// Calls a method on an object by name.
/// </summary>
Expand Down
40 changes: 38 additions & 2 deletions Execution/Interpreter.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ internal async Task<RuntimeValue> EvaluateAsync(Expr expr)
case Expr.Await awaitExpr: return await EvaluateAwaitAsync(awaitExpr);
case Expr.DynamicImport di: return EvaluateDynamicImport(di);
case Expr.ImportMeta im: return EvaluateImportMeta(im);
case Expr.Yield yieldExpr: return EvaluateYield(yieldExpr);
case Expr.Yield yieldExpr: return await EvaluateYieldAsync(yieldExpr);
case Expr.RegexLiteral regex: return RuntimeValue.FromObject(new SharpTSRegExp(regex.Pattern, regex.Flags));
case Expr.ClassExpr classExpr: return EvaluateClassExpression(classExpr);
default: throw new InvalidOperationException($"Runtime Error: Unhandled expression type in async Interpreter: {expr.GetType().Name}");
Expand Down Expand Up @@ -210,6 +210,34 @@ private RuntimeValue EvaluateYield(Expr.Yield yieldExpr)
throw new YieldException(value, yieldExpr.IsDelegating);
}

/// <summary>
/// Evaluates a <c>yield</c>/<c>yield*</c> inside an async generator body. The body runs as an
/// ordinary interpreter async execution, so the yielded expression is evaluated asynchronously
/// (supporting <c>yield await x</c>) and the suspension is delegated to the active async generator
/// (<see cref="Runtime.Types.SharpTSAsyncGenerator.OnYieldAsync"/>), which hands the value to the
/// driving <c>next()</c> and awaits the resume. Falls back to the synchronous <c>yield</c> path when
/// no async generator is active (a misplaced yield reached via the async dispatcher).
/// </summary>
private async Task<RuntimeValue> EvaluateYieldAsync(Expr.Yield yieldExpr)
{
var activeGenerator = CurrentAsyncGenerator;
if (activeGenerator == null)
return EvaluateYield(yieldExpr);

object? value = yieldExpr.Value != null
? (await EvaluateAsync(yieldExpr.Value)).ToObject()
: SharpTSUndefined.Instance;

object? result = await activeGenerator.OnYieldAsync(value, yieldExpr.IsDelegating);

// For a plain `yield`, the resume value is delivered verbatim (so `next(null)` yields null and a
// bare next() yields undefined). For `yield*`, a non-generator delegate's completion value is
// undefined; coalesce null → undefined to preserve that.
if (yieldExpr.IsDelegating)
return RuntimeValue.FromBoxed(result ?? SharpTSUndefined.Instance);
return RuntimeValue.FromBoxed(result);
}

/// <summary>
/// Evaluates an await expression, unwrapping the Promise value.
/// </summary>
Expand Down Expand Up @@ -449,7 +477,15 @@ private RuntimeValue EvaluateArrowFunction(Expr.ArrowFunction arrow)
RuntimeEnvironment closure = _environment;

ISharpTSCallable func;
if (arrow.IsAsync)
if (arrow.IsAsync && arrow.IsGenerator)
{
// Async generator function expressions (async function*) — wrap in an async-generator-
// creating function. Dispatched before the plain-async branch so an async generator
// expression that closes over a block-scoped binding (left in place by GeneratorArrowLifter)
// runs natively rather than being mishandled as a plain async function (#734).
func = new SharpTSAsyncArrowGeneratorFunction(arrow, closure, arrow.HasOwnThis);
}
else if (arrow.IsAsync)
{
func = new SharpTSAsyncArrowFunction(arrow, closure, arrow.HasOwnThis);
}
Expand Down
10 changes: 10 additions & 0 deletions Execution/Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@ public override void Send(SendOrPostCallback d, object? state)
/// </summary>
internal Func<object?, bool, object?>? YieldCallback { get; set; }

/// <summary>
/// The async generator whose body is currently executing, if any. An async generator body runs as
/// an ordinary interpreter async execution on the single event-loop thread; this binding lets the
/// async <c>yield</c> evaluator (<see cref="EvaluateYieldAsync"/>) suspend through the right
/// generator. It is re-asserted across every suspension the body crosses — guest awaits restore it
/// via <see cref="AwaitPreservingEnvironment"/>, and the generator re-asserts it itself at each
/// <c>yield</c> resume — so interleaved async generators never observe each other's binding.
/// </summary>
internal Runtime.Types.SharpTSAsyncGenerator? CurrentAsyncGenerator { get; set; }

/// <summary>
/// Registers a timer for tracking. Called by TimerBuiltIns when creating setTimeout/setInterval.
/// Enables proper cleanup of all pending timers when the interpreter is disposed.
Expand Down
24 changes: 11 additions & 13 deletions Parsing/GeneratorArrowLifter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -760,19 +760,17 @@ private Expr LiftGeneratorArrow(Expr.ArrowFunction af)
{
var freeVars = FreeVariableCollector.Collect(af);

// #678: a generator expression that closes over a block-scoped binding (loop variable, catch
// parameter, or a let/const/class declared in a nested block) cannot be lifted — the module
// body and every enclosing function body sit outside that block, so the lift would unbind the
// reference. Leave it in place as a generator EXPRESSION: the interpreter runs generator
// expressions natively and the type checker establishes the generator context directly. The
// body is still rewritten so any nested generator expressions inside it are handled, and its
// own name (if any) stays bound natively — no #679 self-binding rewrite is needed in place.
//
// ASYNC generator expressions are excluded: the interpreter's native arrow path does not yet
// build an async generator instance (EvaluateArrowFunction treats an async arrow as a plain
// async function), so an in-place async generator would be mishandled. They keep their prior
// lift behavior — a block-capturing async generator still reports "Undefined variable" (#734).
if (!af.IsAsync && CapturesBlockScopedBinding(freeVars))
// #678/#734: a generator expression (sync or async) that closes over a block-scoped binding
// (loop variable, catch parameter, or a let/const/class declared in a nested block) cannot be
// lifted — the module body and every enclosing function body sit outside that block, so the lift
// would unbind the reference. Leave it in place as a generator EXPRESSION: the interpreter runs
// both sync (SharpTSArrowGeneratorFunction) and async (SharpTSAsyncArrowGeneratorFunction)
// generator expressions natively, and the type checker establishes the generator context
// directly (CheckArrowFunction handles arrow.IsAsync). The body is still rewritten so any nested
// generator expressions inside it are handled, and its own name (if any) stays bound natively —
// no #679 self-binding rewrite is needed in place. The compiler has no generator-expression IL
// path and reports a clear "Yield not supported in this context" error for the capturing case.
if (CapturesBlockScopedBinding(freeVars))
{
var inPlaceBody = RewriteFunctionBody(af.Parameters, af.BlockBody!, selfName: af.Name?.Lexeme);
var inPlaceParams = RewriteParameters(af.Parameters);
Expand Down
7 changes: 5 additions & 2 deletions Runtime/BuiltIns/AsyncGeneratorBuiltIns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ public static class AsyncGeneratorBuiltIns
{
return name switch
{
"next" => new BuiltInAsyncMethod("next", 0, 0, async (_, receiver, _) =>
"next" => new BuiltInAsyncMethod("next", 0, 1, async (_, receiver, args) =>
{
if (receiver is SharpTSAsyncGenerator gen)
{
return await gen.Next();
// A resumed `yield` evaluates to the value sent via next(v); an omitted argument is
// undefined, not null (ECMA-262 §27.6.3.6).
object? sent = args.Count > 0 ? args[0] : SharpTSUndefined.Instance;
return await gen.Next(sent);
}
throw new Exception("Runtime Error: next() called on non-async-generator.");
}),
Expand Down
Loading
Loading