diff --git a/src/core/Microsoft.Dynamic/Ast/AsyncDebugSupport.cs b/src/core/Microsoft.Dynamic/Ast/AsyncDebugSupport.cs
new file mode 100644
index 00000000..396583c8
--- /dev/null
+++ b/src/core/Microsoft.Dynamic/Ast/AsyncDebugSupport.cs
@@ -0,0 +1,213 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+using Microsoft.Scripting.Debugging;
+using Microsoft.Scripting.Debugging.CompilerServices;
+
+namespace Microsoft.Scripting.Ast {
+ ///
+ /// Compiles an async body for tracing.
+ ///
+ ///
+ /// Used by and AsyncEnumerableRewriter when the async node carries a debug hook (set by the host
+ /// only when it builds the tracing variant of the code, e.g. when sys.settrace is active). In two steps:
+ ///
+ /// instruments the body before it is sliced into a state machine, so that the debug points
+ /// end up in the same state machine as the awaits and yields.
+ /// wraps the resulting state machine in a , which dispatches
+ /// the trace events while the driver pumps it.
+ ///
+ ///
+ /// The body's locals are not lifted, so its frame reports its location but no locals; the scope map is therefore uniform
+ /// (every sequence point maps to no locals).
+ ///
+ internal sealed class AsyncDebugSupport {
+ private static readonly ConstructorInfo s_tracingStateMachineCtor
+ = typeof(TracingStateMachine).GetConstructor(
+ BindingFlags.Instance | BindingFlags.NonPublic, binder: null,
+ [typeof(IEnumerator), typeof(DebugContext), typeof(FunctionInfo), typeof(StrongBox), typeof(StrongBox), typeof(bool)],
+ modifiers: null)!;
+ private static readonly MethodInfo s_onExceptionCaughtMethod
+ = typeof(TracingStateMachine).GetMethod(nameof(TracingStateMachine.OnExceptionCaught), BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+ private readonly DebugContext _debugContext;
+ private readonly DebugLambdaInfo _lambdaInfo;
+ private readonly string? _name;
+ private readonly ParameterExpression _exceptionSlot;
+
+ // Holds the TracingStateMachine, so that the body's exception handlers can report to it.
+ private readonly ParameterExpression _tracer = Expression.Variable(typeof(TracingStateMachine), "$tracer");
+
+ // Null until the body is instrumented, and also if it has no sequence points (then there is nothing to trace).
+ private FunctionInfo? _functionInfo;
+
+ /// The body's exception slot, which the body rethrows from at each resume point.
+ internal AsyncDebugSupport(DebugContext debugContext, DebugLambdaInfo lambdaInfo, string? name, ParameterExpression exceptionSlot) {
+ _debugContext = debugContext;
+ _lambdaInfo = lambdaInfo;
+ _name = name;
+ _exceptionSlot = exceptionSlot;
+ }
+
+
+ ///
+ /// Instruments to report its sequence points and the exceptions it handles.
+ ///
+ ///
+ /// Each becomes { yield DebugPoint(cookie); rethrow-if-pending } on
+ /// , the label the body's awaits and yields are sliced on, and each exception handler first reports
+ /// the exception it handles. The rethrow lets an exception raised by the trace callback surface inside the body.
+ ///
+ internal Expression InjectDebugPoints(Expression body, LabelTarget yieldLabel) {
+ var injector = new DebugPointInjector(_debugContext, yieldLabel, _exceptionSlot, _tracer);
+ Expression instrumentedBody = injector.Visit(body);
+ if (injector.SequencePoints.Length == 0) {
+ return body;
+ }
+
+ _functionInfo = DebugContext.CreateFunctionInfo(
+ generatorFactory: null, // the frame cannot be remapped to a generator, so it cannot change its location either
+ _name ?? "",
+ injector.SequencePoints,
+ injector.VariableScopeMap,
+ Array.Empty(),
+ _lambdaInfo.CustomPayload);
+ return instrumentedBody;
+ }
+
+
+ ///
+ /// Wraps , the state machine of the instrumented body, in a .
+ ///
+ /// Where the body leaves its return value, or null if it has none.
+ /// Whether the state machine is driven by AsyncHelpers.DriveAsyncEnumerable .
+ internal Expression TraceStateMachine(Expression stateMachine, ParameterExpression? resultSlot, bool isAsyncEnumerable) {
+ if (_functionInfo is null) {
+ return stateMachine;
+ }
+
+ return Expression.Block(
+ typeof(TracingStateMachine),
+ [_tracer],
+ Expression.Assign(
+ _tracer,
+ Expression.New(
+ s_tracingStateMachineCtor,
+ stateMachine,
+ Expression.Constant(_debugContext),
+ Expression.Constant(_functionInfo),
+ (Expression?)resultSlot ?? Expression.Constant(null, typeof(StrongBox)),
+ _exceptionSlot,
+ Expression.Constant(isAsyncEnumerable))));
+ }
+
+
+ ///
+ /// Rewrites the debug info and the exception handlers of an async body for .
+ ///
+ ///
+ /// Each becomes a yield of a on the body's own yield label, recording
+ /// the source span per cookie, and each catch block first reports the exception to the tracer. This mirrors what
+ /// DebugInfoRewriter does to a synchronous body.
+ ///
+ private sealed class DebugPointInjector : ExpressionVisitor {
+ private readonly DebugContext _debugContext;
+ private readonly LabelTarget _yieldLabel;
+ private readonly ParameterExpression _exceptionSlot;
+ private readonly ParameterExpression _tracer;
+ private readonly List _spans = new();
+
+ internal DebugPointInjector(DebugContext debugContext, LabelTarget yieldLabel, ParameterExpression exceptionSlot, ParameterExpression tracer) {
+ _debugContext = debugContext;
+ _yieldLabel = yieldLabel;
+ _exceptionSlot = exceptionSlot;
+ _tracer = tracer;
+ }
+
+ internal DebugSourceSpan[] SequencePoints => _spans.ToArray();
+
+ internal IList[] VariableScopeMap {
+ get {
+ var map = new IList[_spans.Count];
+ for (int i = 0; i < map.Length; i++) {
+ map[i] = Array.Empty();
+ }
+ return map;
+ }
+ }
+
+ protected override Expression VisitDebugInfo(DebugInfoExpression node) {
+ if (node.IsClear) {
+ return Expression.Empty();
+ }
+
+ if (node.Document is null) {
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.CurrentCulture, "DebugInfoExpression at sequence point {0} has no SymbolDocumentInfo", _spans.Count));
+ }
+
+ DebugSourceFile sourceFile = _debugContext.GetDebugSourceFile(
+ string.IsNullOrEmpty(node.Document.FileName) ? "" : node.Document.FileName);
+
+ int cookie = _spans.Count;
+ _spans.Add(new DebugSourceSpan(sourceFile, node.StartLine, node.StartColumn, node.EndLine, node.EndColumn));
+
+ return Expression.Block(
+ typeof(void),
+ Utils.YieldReturn(_yieldLabel, Expression.Constant(new DebugPoint(cookie))),
+ AsyncRewriter.RethrowIfPending(_exceptionSlot));
+ }
+
+ protected override Expression VisitTry(TryExpression node) {
+ var visited = (TryExpression)base.VisitTry(node);
+ if (visited.Handlers.Count == 0) {
+ return visited;
+ }
+
+ var handlers = new List(visited.Handlers.Count);
+ foreach (CatchBlock handler in visited.Handlers) {
+ ParameterExpression variable = handler.Variable ?? Expression.Variable(handler.Test, null);
+ Expression exception = variable.Type == typeof(Exception) ? variable : Expression.Convert(variable, typeof(Exception));
+ handlers.Add(Expression.MakeCatchBlock(
+ handler.Test,
+ variable,
+ Expression.Block(Expression.Call(_tracer, s_onExceptionCaughtMethod, exception), handler.Body),
+ handler.Filter));
+ }
+ return Expression.MakeTry(visited.Type, visited.Body, visited.Finally, visited.Fault, handlers);
+ }
+
+ protected override Expression VisitLambda(Expression node) {
+ // Don't descend into nested lambdas — they have their own debug context.
+ return node;
+ }
+
+ protected override Expression VisitExtension(Expression node) {
+ // A nested async node is sliced into its own state machine against its own label; leave it opaque.
+ if (node is AsyncExpression or GeneratorExpression
+#if NET
+ or AsyncEnumerableExpression
+#endif
+ ) {
+ return node;
+ }
+ if (node is YieldExpression { Value: not null } yield && yield.Target == _yieldLabel) {
+ return Expression.Block(
+ base.VisitExtension(node),
+ AsyncRewriter.RethrowIfPending(_exceptionSlot));
+ }
+ return base.VisitExtension(node);
+ }
+ }
+ }
+}
diff --git a/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableExpression.cs b/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableExpression.cs
index 6c2377cf..fd5b60e7 100644
--- a/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableExpression.cs
+++ b/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableExpression.cs
@@ -13,6 +13,7 @@
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Scripting.Debugging.CompilerServices;
using Microsoft.Scripting.Utils;
namespace Microsoft.Scripting.Ast {
@@ -35,12 +36,33 @@ public sealed class AsyncEnumerableExpression : Expression {
internal AsyncEnumerableExpression(string? name, Expression body, LabelTarget yieldLabel,
Expression? cancellationToken = null,
- Expression? cancellationException = null) {
+ Expression? cancellationException = null,
+ DebugContext? debugContext = null,
+ DebugLambdaInfo? debugLambdaInfo = null) {
Name = name;
Body = body;
YieldLabel = yieldLabel;
CancellationToken = cancellationToken ?? Utils.DefaultCancellationToken;
CancellationException = cancellationException ?? Utils.DefaultCancellationException;
+ DebugContext = debugContext;
+ DebugLambdaInfo = debugLambdaInfo;
+ }
+
+ ///
+ /// Optional tracing hook; see . When both are non-null,
+ /// instruments the body with debug points so it becomes traceable.
+ ///
+ internal DebugContext? DebugContext { get; }
+
+ internal DebugLambdaInfo? DebugLambdaInfo { get; }
+
+ ///
+ /// Returns a copy of this node that instruments its body for tracing under the given debug context.
+ ///
+ public AsyncEnumerableExpression WithDebugInfo(DebugContext debugContext, DebugLambdaInfo debugLambdaInfo) {
+ ContractUtils.RequiresNotNull(debugContext, nameof(debugContext));
+ ContractUtils.RequiresNotNull(debugLambdaInfo, nameof(debugLambdaInfo));
+ return new AsyncEnumerableExpression(Name, Body, YieldLabel, CancellationToken, CancellationException, debugContext, debugLambdaInfo);
}
/// Optional diagnostic name (forwarded to the inner generator).
@@ -76,7 +98,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) {
Expression ct = visitor.Visit(CancellationToken);
Expression ce = visitor.Visit(CancellationException);
if (b == Body && ct == CancellationToken && ce == CancellationException) return this;
- return new AsyncEnumerableExpression(Name, b, YieldLabel, ct, ce);
+ return new AsyncEnumerableExpression(Name, b, YieldLabel, ct, ce, DebugContext, DebugLambdaInfo);
}
}
diff --git a/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableRewriter.cs b/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableRewriter.cs
index 14a78e50..27a3e012 100644
--- a/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableRewriter.cs
+++ b/src/core/Microsoft.Dynamic/Ast/AsyncEnumerableRewriter.cs
@@ -11,7 +11,6 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
-using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
namespace Microsoft.Scripting.Ast {
@@ -28,8 +27,6 @@ private static readonly ConstructorInfo s_awaitPointCtor
= typeof(Runtime.AwaitPoint).GetConstructor([typeof(Task)])!;
private static readonly FieldInfo s_valueSlotField
= typeof(StrongBox).GetField(nameof(StrongBox.Value))!;
- private static readonly FieldInfo s_exceptionSlotField
- = typeof(StrongBox).GetField(nameof(StrongBox.Value))!;
private static readonly ConstructorInfo s_valueSlotCtor
= typeof(StrongBox).GetConstructor(Type.EmptyTypes)!;
private static readonly ConstructorInfo s_exceptionSlotCtor
@@ -48,8 +45,17 @@ public Expression Reduce() {
ParameterExpression valueSlot = Expression.Variable(typeof(StrongBox), "$asyncValue");
ParameterExpression exceptionSlot = Expression.Variable(typeof(StrongBox), "$awaitException");
+ // Optional tracing, as in AsyncRewriter: the debug points are injected on the same label the body's yields and awaits target,
+ // so they all land in one state machine, which is then wrapped in a TracingStateMachine.
+ Expression body = _node.Body;
+ AsyncDebugSupport? debugSupport = null;
+ if (_node.DebugContext is not null && _node.DebugLambdaInfo is not null) {
+ debugSupport = new AsyncDebugSupport(_node.DebugContext, _node.DebugLambdaInfo, _node.Name, exceptionSlot);
+ body = debugSupport.InjectDebugPoints(body, _node.YieldLabel);
+ }
+
var rewriter = new AwaitToAwaitPointRewriter(_node.YieldLabel, valueSlot, exceptionSlot);
- Expression rewrittenBody = rewriter.Visit(_node.Body);
+ Expression rewrittenBody = rewriter.Visit(body);
// Coerce to void for Utils.Generator (the generator body's value is discarded).
Expression generatorBody = rewrittenBody.Type == typeof(void)
@@ -62,12 +68,13 @@ public Expression Reduce() {
generatorBody,
typeof(IEnumerator),
rewriteAssignments: false);
+ Expression states = debugSupport?.TraceStateMachine(generator, resultSlot: null, isAsyncEnumerable: true) ?? generator;
// Argument order matches DriveAsyncEnumerable: ..., cancellationToken, cancellationException
// (same as DriveAsync — cancellationToken is the last required parameter).
Expression drive = Expression.Call(
s_driveMethod,
- generator,
+ states,
valueSlot,
exceptionSlot,
_node.CancellationToken,
@@ -88,11 +95,6 @@ public Expression Reduce() {
/// value yielded by a language-level yield .
///
private sealed class AwaitToAwaitPointRewriter : ExpressionVisitor {
- private static readonly MethodInfo s_captureMethod
- = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Capture))!;
- private static readonly MethodInfo s_throwMethod
- = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Throw), Type.EmptyTypes)!;
-
private readonly LabelTarget _yieldLabel;
private readonly ParameterExpression _valueSlot;
private readonly ParameterExpression _exceptionSlot;
@@ -109,20 +111,12 @@ protected override Expression VisitExtension(Expression node) {
// Wrap the awaited Task in an AwaitPoint marker, then box to object for the yield.
Expression awaitPoint = Expression.New(s_awaitPointCtor, Expression.Convert(operand, typeof(Task)));
Expression yielded = Expression.Convert(awaitPoint, typeof(object));
-
- Expression readException = Expression.Field(_exceptionSlot, s_exceptionSlotField);
Expression readSlot = Expression.Field(_valueSlot, s_valueSlotField);
- Expression rethrow = Expression.IfThen(
- Expression.ReferenceNotEqual(readException, Expression.Constant(null, typeof(Exception))),
- Expression.Call(
- Expression.Call(s_captureMethod, readException),
- s_throwMethod));
-
return Expression.Block(
typeof(object),
Utils.YieldReturn(_yieldLabel, yielded),
- rethrow,
+ AsyncRewriter.RethrowIfPending(_exceptionSlot),
readSlot);
}
return base.VisitExtension(node);
diff --git a/src/core/Microsoft.Dynamic/Ast/AsyncExpression.cs b/src/core/Microsoft.Dynamic/Ast/AsyncExpression.cs
index 49edab19..2dc0b309 100644
--- a/src/core/Microsoft.Dynamic/Ast/AsyncExpression.cs
+++ b/src/core/Microsoft.Dynamic/Ast/AsyncExpression.cs
@@ -10,6 +10,7 @@
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Scripting.Debugging.CompilerServices;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Utils;
@@ -39,11 +40,33 @@ public sealed class AsyncExpression : Expression {
internal AsyncExpression(string? name, Expression body,
Expression? cancellationToken = null,
- Expression? cancellationException = null) {
+ Expression? cancellationException = null,
+ DebugContext? debugContext = null,
+ DebugLambdaInfo? debugLambdaInfo = null) {
Name = name;
Body = body;
CancellationToken = cancellationToken ?? Utils.DefaultCancellationToken;
CancellationException = cancellationException ?? Utils.DefaultCancellationException;
+ DebugContext = debugContext;
+ DebugLambdaInfo = debugLambdaInfo;
+ }
+
+ ///
+ /// Optional tracing hook. When both are non-null, instruments the body with debug
+ /// points so it becomes traceable (see AsyncDebugSupport ); when null, reduction is unchanged.
+ /// Set by the host (e.g. IronPython's FunctionCode ) only when building the tracing variant.
+ ///
+ internal DebugContext? DebugContext { get; }
+
+ internal DebugLambdaInfo? DebugLambdaInfo { get; }
+
+ ///
+ /// Returns a copy of this node that instruments its body for tracing under the given debug context.
+ ///
+ public AsyncExpression WithDebugInfo(DebugContext debugContext, DebugLambdaInfo debugLambdaInfo) {
+ ContractUtils.RequiresNotNull(debugContext, nameof(debugContext));
+ ContractUtils.RequiresNotNull(debugLambdaInfo, nameof(debugLambdaInfo));
+ return new AsyncExpression(Name, Body, CancellationToken, CancellationException, debugContext, debugLambdaInfo);
}
///
@@ -86,7 +109,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) {
Expression ct = visitor.Visit(CancellationToken);
Expression ce = visitor.Visit(CancellationException);
if (b == Body && ct == CancellationToken && ce == CancellationException) return this;
- return new AsyncExpression(Name, b, ct, ce);
+ return new AsyncExpression(Name, b, ct, ce, DebugContext, DebugLambdaInfo);
}
}
diff --git a/src/core/Microsoft.Dynamic/Ast/AsyncRewriter.cs b/src/core/Microsoft.Dynamic/Ast/AsyncRewriter.cs
index ee52b13f..7c15a416 100644
--- a/src/core/Microsoft.Dynamic/Ast/AsyncRewriter.cs
+++ b/src/core/Microsoft.Dynamic/Ast/AsyncRewriter.cs
@@ -29,6 +29,10 @@ private static readonly ConstructorInfo s_valueSlotCtor
= typeof(StrongBox).GetConstructor(Type.EmptyTypes)!;
private static readonly ConstructorInfo s_exceptionSlotCtor
= typeof(StrongBox).GetConstructor(Type.EmptyTypes)!;
+ private static readonly MethodInfo s_captureMethod
+ = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Capture))!;
+ private static readonly MethodInfo s_throwMethod
+ = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Throw), Type.EmptyTypes)!;
private readonly AsyncExpression _node;
@@ -52,9 +56,18 @@ public Expression Reduce() {
ParameterExpression exceptionSlot = Expression.Variable(typeof(StrongBox), "$awaitException");
LabelTarget yieldLabel = Expression.Label(typeof(object), "$asyncYield");
+ // Optional tracing, when the node carries a debug hook: instrument the body before the await rewrite, so that its debug points
+ // are sliced into the same state machine as the awaits, and wrap that state machine in a TracingStateMachine (see below).
+ Expression body = _node.Body;
+ AsyncDebugSupport? debugSupport = null;
+ if (_node.DebugContext is not null && _node.DebugLambdaInfo is not null) {
+ debugSupport = new AsyncDebugSupport(_node.DebugContext, _node.DebugLambdaInfo, _node.Name, exceptionSlot);
+ body = debugSupport.InjectDebugPoints(body, yieldLabel);
+ }
+
// Rewrite AwaitExpression(e) -> { yield e; rethrow-if-pending; valueSlot.Value }
var rewriter = new AwaitToYieldRewriter(yieldLabel, valueSlot, exceptionSlot);
- Expression rewrittenBody = rewriter.Visit(_node.Body);
+ Expression rewrittenBody = rewriter.Visit(body);
// After the body completes, the function's final value must live in valueSlot for DriveAsync to pick up. For a value-typed body this
// is a single assignment of the body expression into the slot. For a void body, the body has no value to assign, but we still
@@ -81,10 +94,11 @@ public Expression Reduce() {
generatorBody,
typeof(IEnumerator),
rewriteAssignments: false);
+ Expression states = debugSupport?.TraceStateMachine(generator, resultSlot: valueSlot, isAsyncEnumerable: false) ?? generator;
Expression drive = Expression.Call(
s_driveMethod,
- generator,
+ states,
valueSlot,
exceptionSlot,
_node.CancellationToken,
@@ -98,12 +112,25 @@ public Expression Reduce() {
drive);
}
- private sealed class AwaitToYieldRewriter : ExpressionVisitor {
- private static readonly MethodInfo s_captureMethod
- = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Capture))!;
- private static readonly MethodInfo s_throwMethod
- = typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Throw), Type.EmptyTypes)!;
+ ///
+ /// Rethrows the exception parked in , if any, preserving its original stack trace.
+ ///
+ ///
+ /// Emitted right after each yield of an async body: whatever resumes the body (the driver past an await, or the tracer past a
+ /// debug point) parks an exception in the slot to make the body observe it at the resume point.
+ ///
+ internal static Expression RethrowIfPending(Expression exceptionSlot) {
+ Expression readException = Expression.Field(exceptionSlot, s_exceptionSlotField);
+ return Expression.IfThen(
+ Expression.ReferenceNotEqual(readException, Expression.Constant(null, typeof(Exception))),
+ Expression.Call(
+ Expression.Call(s_captureMethod, readException),
+ s_throwMethod));
+ }
+
+
+ private sealed class AwaitToYieldRewriter : ExpressionVisitor {
private readonly LabelTarget _yieldLabel;
private readonly ParameterExpression _resultSlot;
private readonly ParameterExpression _exceptionSlot;
@@ -118,22 +145,15 @@ protected override Expression VisitExtension(Expression node) {
if (node is AwaitExpression aw) {
Expression operand = Visit(aw.Operand);
Expression boxed = operand.Type == typeof(object) ? operand : Expression.Convert(operand, typeof(object));
- Expression readException = Expression.Field(_exceptionSlot, s_exceptionSlotField);
Expression readSlot = Expression.Field(_resultSlot, s_valueSlotField);
- // After the yield, if the runner stored an exception, rethrow it preserving the original stack trace
+ // After the yield, if the runner stored an exception, rethrow it preserving the original stack trace
// (so the body's try/except observes the right exception object).
// Otherwise return the awaited result.
- Expression rethrow = Expression.IfThen(
- Expression.ReferenceNotEqual(readException, Expression.Constant(null, typeof(Exception))),
- Expression.Call(
- Expression.Call(s_captureMethod, readException),
- s_throwMethod));
-
return Expression.Block(
typeof(object),
Utils.YieldReturn(_yieldLabel, boxed),
- rethrow,
+ RethrowIfPending(_exceptionSlot),
readSlot);
}
return base.VisitExtension(node);
diff --git a/src/core/Microsoft.Dynamic/Ast/GeneratorRewriter.cs b/src/core/Microsoft.Dynamic/Ast/GeneratorRewriter.cs
index 013aab28..6358ee23 100644
--- a/src/core/Microsoft.Dynamic/Ast/GeneratorRewriter.cs
+++ b/src/core/Microsoft.Dynamic/Ast/GeneratorRewriter.cs
@@ -324,6 +324,11 @@ protected override Expression VisitTry(TryExpression node) {
if (catchYields != tryYields) {
var block = new List();
+ // A deferred handler runs if its variable is set, so the variables must be reset on each entry into the try: they are
+ // hoisted, and would otherwise keep the exception of an earlier pass (e.g. of a loop) whose handler did not reach its end.
+ // A resumption into a deferred handler is routed past the reset, which keeps the exception for the rest of the handler.
+ var resetDeferredVars = new List();
+
block.Add(MakeYieldRouter(node.Body.Type, tryYields, catchYields, tryStart));
block.Add(null); // empty slot to fill in later
@@ -347,6 +352,7 @@ protected override Expression VisitTry(TryExpression node) {
// the catch might contain yields.
var deferredVar = c.Variable ?? Expression.Variable(c.Test, null);
_vars.Add(deferredVar);
+ resetDeferredVars.Add(Expression.Assign(deferredVar, AstUtils.Constant(null, deferredVar.Type)));
// We need to ensure that filters can access the exception
// variable
@@ -382,7 +388,8 @@ protected override Expression VisitTry(TryExpression node) {
);
}
- block[1] = Expression.MakeTry(null, @try, null, null, new ReadOnlyCollection(handlers));
+ resetDeferredVars.Add(Expression.MakeTry(null, @try, null, null, new ReadOnlyCollection(handlers)));
+ block[1] = Expression.Block(resetDeferredVars);
@try = Expression.Block(block);
handlers = EmptyArray.Instance; // so we don't reuse these
}
@@ -535,7 +542,8 @@ protected override Expression VisitExtension(Expression node) {
// Lowered async nodes (AsyncExpression / AsyncEnumerableExpression) carry their own generator
// label and are reduced — with their await/yield points rewritten — in their own context so skip here.
- // TODO: tracing *into* async bodies will instead require dedicated support here rather than this skip.
+ // Tracing into an async body does not need this rewriter either: the node instruments its own body when
+ // reduced, if it carries a debug hook (AsyncExpression.WithDebugInfo).
if (node is AsyncExpression
#if NET
or AsyncEnumerableExpression
diff --git a/src/core/Microsoft.Dynamic/Debugging/DebugInfoRewriter.cs b/src/core/Microsoft.Dynamic/Debugging/DebugInfoRewriter.cs
index b660f509..081b5ce5 100644
--- a/src/core/Microsoft.Dynamic/Debugging/DebugInfoRewriter.cs
+++ b/src/core/Microsoft.Dynamic/Debugging/DebugInfoRewriter.cs
@@ -123,8 +123,8 @@ protected override Expression VisitLambda(MSAst.Expression node) {
protected override Expression VisitExtension(Expression node) {
// Lowered async nodes (AsyncExpression / AsyncEnumerableExpression) are opaque boundaries to the
// debug transform, like nested lambdas (see VisitLambda). The async body is sliced into its own
- // state machine, against its own generator label, when the node is reduced in its own context.
- // TODO: make async bodies traceable via dedicated support here rather than skipping them.
+ // state machine, against its own generator label, when the node is reduced in its own context;
+ // a host traces the async body by handing the node its own debug hook (AsyncExpression.WithDebugInfo) instead.
if (node is Microsoft.Scripting.Ast.AsyncExpression
#if NET
or Microsoft.Scripting.Ast.AsyncEnumerableExpression
diff --git a/src/core/Microsoft.Dynamic/Debugging/TracePipeline/TracePipeline.cs b/src/core/Microsoft.Dynamic/Debugging/TracePipeline/TracePipeline.cs
index 76df56de..ad55658a 100644
--- a/src/core/Microsoft.Dynamic/Debugging/TracePipeline/TracePipeline.cs
+++ b/src/core/Microsoft.Dynamic/Debugging/TracePipeline/TracePipeline.cs
@@ -60,6 +60,11 @@ public bool TrySetNextStatement(string sourceFile, SourceSpan sourceSpan) {
if (sequencePointIndex == Int32.MaxValue)
return false;
+ // Changing the location remaps the frame to a generator; a frame without a generator factory (a traced async body) cannot do that.
+ if (traceFrame.Generator is null && traceFrame.FunctionInfo.GeneratorFactory is null &&
+ (sequencePointIndex != traceFrame.CurrentSequencePointIndex || traceFrame.ThrownException is not null))
+ return false;
+
traceFrame.CurrentSequencePointIndex = sequencePointIndex;
return true;
}
diff --git a/src/core/Microsoft.Dynamic/Debugging/TracingStateMachine.cs b/src/core/Microsoft.Dynamic/Debugging/TracingStateMachine.cs
new file mode 100644
index 00000000..bfa3d8b4
--- /dev/null
+++ b/src/core/Microsoft.Dynamic/Debugging/TracingStateMachine.cs
@@ -0,0 +1,230 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+
+using Microsoft.Scripting.Debugging.CompilerServices;
+using Microsoft.Scripting.Runtime;
+
+namespace Microsoft.Scripting.Debugging {
+ ///
+ /// Wraps the state machine of an async body compiled for tracing, and dispatches the body's trace events.
+ ///
+ ///
+ /// It sits between the body's state machine and its driver ( or
+ /// AsyncHelpers.DriveAsyncEnumerable ), and is inserted only when the body is compiled for tracing
+ /// (see ), so the drivers know nothing about tracing. It consumes the
+ /// s that the instrumented body yields at its sequence points and passes everything else through.
+ ///
+ /// The body's frame lives exactly as long as the body runs, as for a generator in CPython: one is one
+ /// resumption, so the frame is pushed and call fired when the body resumes, and return fired and the frame popped
+ /// when the body suspends, completes, or faults. A suspended body is therefore never on a thread's frame stack, whatever runs in
+ /// the meantime and whichever thread the body resumes on. The one exception is a suspension that the driver resumes synchronously
+ /// (an await of an already completed task): then the frame stays open, since no other code runs in between and CPython does not
+ /// suspend there either.
+ ///
+ /// An exception raised by the trace callback when the body is about to resume is not thrown out of the driver. It is handed
+ /// to the body through the exception slot, the same way the driver delivers a faulted await, so it surfaces inside the body, where
+ /// the body's own try statements see it.
+ ///
+ /// The frame is read-only: it reports the body's location, but none of its locals.
+ ///
+ internal sealed class TracingStateMachine : IEnumerator {
+ // The entry sequence point is the location of the 'call' event, so it fires no 'line' event of its own (as in DebuggableLambdaBuilder).
+ private const int EntryCookie = 0;
+
+ private readonly IEnumerator _states;
+ private readonly DebugContext _debugContext;
+ private readonly MarkerRuntimeVariables _variables;
+ private readonly StrongBox? _resultSlot;
+ private readonly StrongBox _exceptionSlot;
+ private readonly bool _isAsyncEnumerable;
+
+ // The thread the frame is open on; null while the body is suspended.
+ private DefaultDebugThread? _thread;
+
+ /// The state machine of the instrumented body.
+ /// The debug context the trace events are dispatched through.
+ /// The body's sequence points, indexed by the cookies of its s.
+ /// Where the body leaves its return value on completion, or null if it has none (an async generator).
+ /// The slot the body reads at each resume point, and rethrows from if it is set.
+ ///
+ /// Whether the driver is AsyncHelpers.DriveAsyncEnumerable , which hands every item but an to the
+ /// consumer, rather than , which awaits every item.
+ ///
+ internal TracingStateMachine(IEnumerator states, DebugContext debugContext, FunctionInfo functionInfo,
+ StrongBox? resultSlot, StrongBox exceptionSlot, bool isAsyncEnumerable) {
+ _states = states;
+ _debugContext = debugContext;
+ _variables = new MarkerRuntimeVariables(functionInfo);
+ _resultSlot = resultSlot;
+ _exceptionSlot = exceptionSlot;
+ _isAsyncEnumerable = isAsyncEnumerable;
+ }
+
+
+ public object? Current => _states.Current;
+
+ object? IEnumerator.Current => Current;
+
+
+ public bool MoveNext() {
+ if (_thread is null) {
+ Open();
+ }
+
+ while (true) {
+ bool moved;
+ try {
+ moved = _states.MoveNext();
+ } catch (Exception ex) {
+ // The 'exception' event proper has already been fired by the body's handlers (see OnExceptionCaught);
+ // like a synchronous debuggable body, the frame only reports here that it is being unwound.
+ try {
+ Dispatch(TraceEventKind.ExceptionUnwind, ex);
+ } finally {
+ Close(returnValue: null);
+ }
+ throw;
+ }
+
+ if (!moved) {
+ Close(_resultSlot?.Value);
+ return false;
+ }
+
+ object? item = _states.Current;
+ if (item is not DebugPoint debugPoint) {
+ if (!ResumesSynchronously(item)) {
+ Close(item is AwaitPoint awaitPoint ? awaitPoint.Task : item);
+ }
+ return true;
+ }
+
+ _variables.Marker = debugPoint.Cookie;
+ if (debugPoint.Cookie != EntryCookie) {
+ // This resumes the body past a debug point, so, like the driver resuming it past an await, it always writes the slot.
+ _exceptionSlot.Value = TryDispatch(TraceEventKind.TracePoint);
+ }
+ }
+ }
+
+
+ public void Reset() {
+ throw new NotSupportedException();
+ }
+
+
+ public void Dispose() {
+ _states.Dispose();
+ }
+
+
+ ///
+ /// Fires the exception event for an exception the body is about to handle.
+ ///
+ ///
+ /// Called by the instrumented body on entering each exception handler, the way a synchronous debuggable body reports
+ /// first-chance exceptions. An IronPython function body handles every exception at least once (to update its traceback), so this
+ /// also reports an exception that eventually propagates out of the body.
+ ///
+ internal void OnExceptionCaught(Exception exception) {
+ Debug.Assert(_thread is not null, "the body only runs while its frame is open");
+ Dispatch(TraceEventKind.Exception, exception);
+ }
+
+
+ private void Open() {
+ _thread = (DefaultDebugThread)_debugContext.GetCurrentThread();
+ _thread.LiftVariables(_variables);
+
+ // At the very first resumption the marker is the entry point, and the body yields it before reading the slot;
+ // at later resumptions the body is resuming past a suspension and reads the slot immediately.
+ if (TryDispatch(TraceEventKind.FrameEnter) is { } exception) {
+ _exceptionSlot.Value = exception;
+ }
+ }
+
+
+ private void Close(object? returnValue) {
+ try {
+ Dispatch(TraceEventKind.FrameExit, returnValue);
+ } finally {
+ _thread!.PopFrame();
+ _thread = null;
+ }
+ }
+
+
+ ///
+ /// Whether the driver resumes the body right away after , with no other code running in between.
+ ///
+ private bool ResumesSynchronously(object? item) {
+ return _isAsyncEnumerable
+ ? item is AwaitPoint { Task.IsCompleted: true } // anything else is a value handed over to the consumer
+ : item is not Task { IsCompleted: false }; // DriveAsync forwards a non-Task value straight back
+ }
+
+
+ private void Dispatch(TraceEventKind kind, object? payload = null) {
+ _debugContext.DispatchDebugEvent(_thread!, _variables.Marker, kind, payload);
+ }
+
+
+ private Exception? TryDispatch(TraceEventKind kind) {
+ try {
+ Dispatch(kind);
+ return null;
+ } catch (Exception ex) {
+ return ex;
+ }
+ }
+
+
+ ///
+ /// The runtime variables of the body's frame: the body's and its current sequence point.
+ ///
+ ///
+ /// Slot 0 is the and slot 1 the marker, which is the layout expects.
+ /// No further slots are exposed, so the frame reports no locals.
+ ///
+ private sealed class MarkerRuntimeVariables : IRuntimeVariables {
+ private readonly FunctionInfo _functionInfo;
+
+ internal MarkerRuntimeVariables(FunctionInfo functionInfo) {
+ _functionInfo = functionInfo;
+ }
+
+ internal int Marker { get; set; }
+
+ public int Count => 2;
+
+ public object? this[int index] {
+ get => index == 0 ? _functionInfo : Marker;
+ set {
+ if (index == 1) Marker = (int)value!;
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// Marker that an async body compiled for tracing yields at each of its sequence points.
+ ///
+ ///
+ /// It is consumed by , so the driver never sees it. indexes the sequence points
+ /// of the body's .
+ ///
+ internal sealed class DebugPoint(int cookie) {
+ public int Cookie { get; } = cookie;
+ }
+}