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
68 changes: 3 additions & 65 deletions Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -964,71 +964,9 @@ private static bool ContainsSuspensionInExpr(Expr expr)
}
}

/// <summary>
/// Detects return/break/continue that would transfer control out of the surrounding try
/// region. Over-approximates conservatively (labeled break/continue are always treated as
/// escaping): a false positive only costs a statement some mini-segment exception coverage,
/// whereas a false negative would emit a `br`/`ret` inside a protected region (illegal IL).
/// Nested function/arrow bodies are not traversed (their returns are their own).
/// </summary>
private static bool ContainsEscapingExit(Stmt stmt, bool insideLoop, bool insideSwitch)
{
switch (stmt)
{
case Stmt.Return:
return true;
case Stmt.Break b:
return b.Label != null || !(insideLoop || insideSwitch);
case Stmt.Continue c:
return c.Label != null || !insideLoop;
case Stmt.If i:
return ContainsEscapingExit(i.ThenBranch, insideLoop, insideSwitch)
|| (i.ElseBranch != null && ContainsEscapingExit(i.ElseBranch, insideLoop, insideSwitch));
case Stmt.Block b:
if (b.Statements == null) return false;
foreach (var s in b.Statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
case Stmt.Sequence seq:
foreach (var s in seq.Statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
case Stmt.While w:
return ContainsEscapingExit(w.Body, insideLoop: true, insideSwitch);
case Stmt.DoWhile dw:
return ContainsEscapingExit(dw.Body, insideLoop: true, insideSwitch);
case Stmt.For f:
return ContainsEscapingExit(f.Body, insideLoop: true, insideSwitch);
case Stmt.ForOf fo:
return ContainsEscapingExit(fo.Body, insideLoop: true, insideSwitch);
case Stmt.ForIn fi:
return ContainsEscapingExit(fi.Body, insideLoop: true, insideSwitch);
case Stmt.Switch s:
foreach (var c in s.Cases)
foreach (var cs in c.Body)
if (ContainsEscapingExit(cs, insideLoop, insideSwitch: true)) return true;
if (s.DefaultBody != null)
foreach (var ds in s.DefaultBody)
if (ContainsEscapingExit(ds, insideLoop, insideSwitch: true)) return true;
return false;
case Stmt.LabeledStatement ls:
return ContainsEscapingExit(ls.Statement, insideLoop, insideSwitch);
case Stmt.TryCatch t:
if (ContainsEscapingExit2(t.TryBlock, insideLoop, insideSwitch)) return true;
if (t.CatchBlock != null && ContainsEscapingExit2(t.CatchBlock, insideLoop, insideSwitch)) return true;
if (t.FinallyBlock != null && ContainsEscapingExit2(t.FinallyBlock, insideLoop, insideSwitch)) return true;
return false;
default:
return false;
}
}

private static bool ContainsEscapingExit2(List<Stmt> statements, bool insideLoop, bool insideSwitch)
{
foreach (var s in statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
}
// ContainsEscapingExit / ContainsEscapingExit2 are shared across the suspension-aware emitters and
// live in StatementEmitterBase (the generator, async-generator, and async-function emitters all
// segment a flag-based try body around non-local exits using the same conservative analysis).

#endregion
}
17 changes: 17 additions & 0 deletions Compilation/AsyncMoveNextEmitter.Statements.ControlFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,22 @@ protected override void EmitReturn(Stmt.Return r)
_il.Emit(OpCodes.Leave, _setResultLabel);
}

/// <summary>
/// Branch out to <paramref name="target"/> (a loop's break/continue label). Inside a real IL
/// exception block a <c>br</c> out is illegal IL (BranchOutOfTry), so use <c>Leave</c> — which exits
/// the block legally and runs its (no-await) finally. <c>ExceptionBlockDepth</c> counts only real
/// blocks opened by <see cref="EmitSimpleTryCatch"/>, not the flag-based path's mini try/catch
/// segments (EmitSegmentInTry), so an escaping break/continue in a try-with-awaits is pulled out as a
/// segment-breaker (emitted at the top level, depth 0 → <c>Br</c>) while a break targeting a loop
/// nested inside a segment stays a legal in-segment <c>Br</c>. Mirrors the generator emitters (#727).
/// </summary>
protected override void EmitBranchToLabel(Label target)
{
if (_ctx!.ExceptionBlockDepth > 0)
_il.Emit(OpCodes.Leave, target);
else
_il.Emit(OpCodes.Br, target);
}

// EmitIf: inherited from StatementEmitterBase (identical logic)
}
11 changes: 11 additions & 0 deletions Compilation/AsyncMoveNextEmitter.Statements.LabeledLoops.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ private void EmitLabeledWhile(IReadOnlyList<string> labelNames, Stmt.While w, La

private void EmitLabeledForOf(IReadOnlyList<string> labelNames, Stmt.ForOf f, Label outerBreakLabel)
{
if (f.IsAsync)
{
// for await: route to the suspending async-iterator lowering, carrying the chain's labels so
// `break`/`continue <label>` target this loop. Its natural end falls through to
// outerBreakLabel (marked right after by EmitLabeledStatement), so an early break runs the
// iterator's return() cleanup, then exits. The previous synchronous enumeration here ignored
// f.IsAsync and left the reserved await-state labels unmarked → compile failure (#728).
EmitForAwaitOf(f, labelNames);
return;
}

string varName = f.Variable.Lexeme;
var varField = _builder.GetVariableField(varName);

Expand Down
24 changes: 21 additions & 3 deletions Compilation/AsyncMoveNextEmitter.Statements.Loops.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,16 @@ protected override void EmitForOf(Stmt.ForOf f)
/// suspension state for each await (AsyncStateAnalyzer.VisitForOf), consumed below in the same order.
/// The base lowering still serves async arrows and async generators (the latter tracked by #697).
/// </summary>
protected override void EmitForAwaitOf(Stmt.ForOf f)
protected override void EmitForAwaitOf(Stmt.ForOf f) => EmitForAwaitOf(f, labelNames: null);

/// <summary>
/// Emits a <c>for await…of</c> loop, optionally carrying the wrapping statement labels
/// <paramref name="labelNames"/> (a chain <c>a: b: for await</c> contributes several) so a labeled
/// <c>break</c>/<c>continue &lt;label&gt;</c> targets it. The labeled path delegates here so it gets the
/// same suspending async-iterator lowering as the unlabeled case, rather than enumerating the async
/// iterator synchronously and leaving the reserved await-state labels unmarked — #728.
/// </summary>
private void EmitForAwaitOf(Stmt.ForOf f, IReadOnlyList<string>? labelNames)
{
// for await…of drives an async iterator: resolve it (Symbol.asyncIterator, else assume the
// value is itself an async iterator / $IAsyncGenerator), then each iteration await
Expand Down Expand Up @@ -122,7 +131,13 @@ void EmitPrelude()
var continueLabel = _il.DefineLabel();

// break → cleanup (await iterator.return()); natural done → endLabel (no return() per spec).
EnterLoop(cleanupLabel, continueLabel);
// The chain's labels (when present) let `break`/`continue <label>` resolve to this loop; each is
// registered as its own scope at the same targets (the #704 multi-label convention).
bool labeled = labelNames is { Count: > 0 };
if (labeled)
EnterLabeledLoop(cleanupLabel, continueLabel, labelNames!);
else
EnterLoop(cleanupLabel, continueLabel);

_il.MarkLabel(startLabel);

Expand Down Expand Up @@ -174,7 +189,10 @@ void EmitPrelude()
_il.Emit(OpCodes.Pop);

_il.MarkLabel(endLabel);
ExitLoop();
if (labeled)
ExitLabeledLoop(labelNames!);
else
ExitLoop();
}

/// <summary>
Expand Down
73 changes: 40 additions & 33 deletions Compilation/AsyncMoveNextEmitter.Statements.TryCatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ protected override void EmitTryCatch(Stmt.TryCatch t)

private void EmitSimpleTryCatch(Stmt.TryCatch t)
{
// A real IL protected region is open across the whole try/catch/finally. A `br`/`ret` directly
// out of it is illegal, so a non-local break/continue crossing it must use `Leave` instead —
// which also runs this (no-await) finally. ExceptionBlockDepth drives the Leave-vs-Br choice in
// EmitBranchToLabel; it is incremented only here (not in the flag path's mini try/catch
// segments), so a break targeting a loop nested inside the try stays a legal in-region `Br`
// while an escaping break/continue leaves via `Leave` (#727).
_ctx!.ExceptionBlockDepth++;
_il.BeginExceptionBlock();

// Emit try block statements
Expand Down Expand Up @@ -68,6 +75,7 @@ private void EmitSimpleTryCatch(Stmt.TryCatch t)
}

_il.EndExceptionBlock();
_ctx!.ExceptionBlockDepth--;
}

private void EmitTryCatchWithAwaits(Stmt.TryCatch t, bool hasAwaitsInTry, bool hasAwaitsInCatch, bool hasAwaitsInFinally)
Expand Down Expand Up @@ -276,53 +284,52 @@ private void EmitTryBodyWithAwaits(List<Stmt> tryBody, LocalBuilder caughtExcept
_currentTryCatchExceptionLocal = caughtExceptionLocal;
_currentTryCatchSkipLabel = afterTryLabel;

List<Stmt> stmtsBeforeAwait = [];
List<Stmt> syncSegment = [];

void FlushSegment()
{
if (syncSegment.Count == 0)
return;
// Skip the segment if an earlier one already threw (its exception heads to the catch).
var skipSegmentLabel = _il.DefineLabel();
_il.Emit(OpCodes.Ldloc, caughtExceptionLocal);
_il.Emit(OpCodes.Brtrue, skipSegmentLabel);
EmitSegmentInTry(syncSegment, caughtExceptionLocal);
_il.MarkLabel(skipSegmentLabel);
syncSegment.Clear();
}

foreach (var stmt in tryBody)
{
if (ContainsAwait([stmt]))
// A "segment breaker" must be emitted at the top level rather than inside a mini IL
// try/catch: a suspension point (await) whose resume label can't be branched into a
// protected region, or a non-local exit (break/continue/return) whose `Br`/`Leave` out of
// the try targets the enclosing loop — both illegal inside the segment's real IL block. An
// escaping break/continue branches with `Br` at this top level (ExceptionBlockDepth is 0),
// which is what makes it legal; previously it landed inside a segment and `Br`'d out of the
// mini try (BranchOutOfTry → invalid IL) (#727).
if (ContainsAwait([stmt]) || ContainsEscapingExit(stmt, insideLoop: false, insideSwitch: false))
{
// Emit accumulated statements in a try block
if (stmtsBeforeAwait.Count > 0)
{
// Check if exception was already caught
var skipSegmentLabel = _il.DefineLabel();
_il.Emit(OpCodes.Ldloc, caughtExceptionLocal);
_il.Emit(OpCodes.Brtrue, skipSegmentLabel);

EmitSegmentInTry(stmtsBeforeAwait, caughtExceptionLocal);
_il.MarkLabel(skipSegmentLabel);
stmtsBeforeAwait.Clear();
}

// Check if exception was caught before continuing with await
var skipAwaitLabel = _il.DefineLabel();
FlushSegment();

// If an earlier segment threw, skip this suspension/exit and fall through to the catch.
var skipLabel = _il.DefineLabel();
_il.Emit(OpCodes.Ldloc, caughtExceptionLocal);
_il.Emit(OpCodes.Brtrue, skipAwaitLabel);
_il.Emit(OpCodes.Brtrue, skipLabel);

// Emit the statement containing await
// EmitAwait will check _currentTryCatchExceptionLocal and wrap GetResult in try/catch
// EmitAwait checks _currentTryCatchExceptionLocal and wraps GetResult in try/catch; a
// break/continue/return emits its jump here, outside any real IL exception block.
EmitStatement(stmt);

_il.MarkLabel(skipAwaitLabel);
_il.MarkLabel(skipLabel);
}
else
{
stmtsBeforeAwait.Add(stmt);
syncSegment.Add(stmt);
}
}

// Emit remaining statements in a try block
if (stmtsBeforeAwait.Count > 0)
{
// Check if exception was already caught
var skipLabel = _il.DefineLabel();
_il.Emit(OpCodes.Ldloc, caughtExceptionLocal);
_il.Emit(OpCodes.Brtrue, skipLabel);

EmitSegmentInTry(stmtsBeforeAwait, caughtExceptionLocal);
_il.MarkLabel(skipLabel);
}
FlushSegment();

_il.MarkLabel(afterTryLabel);

Expand Down
68 changes: 3 additions & 65 deletions Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1101,71 +1101,9 @@ private static bool ContainsYieldInExpr(Expr expr)
}
}

/// <summary>
/// Detects return/break/continue that would transfer control out of the surrounding try
/// region. Over-approximates conservatively (labeled break/continue are always treated as
/// escaping): a false positive only costs a statement some mini-segment exception coverage,
/// whereas a false negative would emit a `br`/`ret` inside a protected region (illegal IL).
/// Nested function/arrow bodies are not traversed (their returns are their own).
/// </summary>
private static bool ContainsEscapingExit(Stmt stmt, bool insideLoop, bool insideSwitch)
{
switch (stmt)
{
case Stmt.Return:
return true;
case Stmt.Break b:
return b.Label != null || !(insideLoop || insideSwitch);
case Stmt.Continue c:
return c.Label != null || !insideLoop;
case Stmt.If i:
return ContainsEscapingExit(i.ThenBranch, insideLoop, insideSwitch)
|| (i.ElseBranch != null && ContainsEscapingExit(i.ElseBranch, insideLoop, insideSwitch));
case Stmt.Block b:
if (b.Statements == null) return false;
foreach (var s in b.Statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
case Stmt.Sequence seq:
foreach (var s in seq.Statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
case Stmt.While w:
return ContainsEscapingExit(w.Body, insideLoop: true, insideSwitch);
case Stmt.DoWhile dw:
return ContainsEscapingExit(dw.Body, insideLoop: true, insideSwitch);
case Stmt.For f:
return ContainsEscapingExit(f.Body, insideLoop: true, insideSwitch);
case Stmt.ForOf fo:
return ContainsEscapingExit(fo.Body, insideLoop: true, insideSwitch);
case Stmt.ForIn fi:
return ContainsEscapingExit(fi.Body, insideLoop: true, insideSwitch);
case Stmt.Switch s:
foreach (var c in s.Cases)
foreach (var cs in c.Body)
if (ContainsEscapingExit(cs, insideLoop, insideSwitch: true)) return true;
if (s.DefaultBody != null)
foreach (var ds in s.DefaultBody)
if (ContainsEscapingExit(ds, insideLoop, insideSwitch: true)) return true;
return false;
case Stmt.LabeledStatement ls:
return ContainsEscapingExit(ls.Statement, insideLoop, insideSwitch);
case Stmt.TryCatch t:
if (ContainsEscapingExit2(t.TryBlock, insideLoop, insideSwitch)) return true;
if (t.CatchBlock != null && ContainsEscapingExit2(t.CatchBlock, insideLoop, insideSwitch)) return true;
if (t.FinallyBlock != null && ContainsEscapingExit2(t.FinallyBlock, insideLoop, insideSwitch)) return true;
return false;
default:
return false;
}
}

private static bool ContainsEscapingExit2(List<Stmt> statements, bool insideLoop, bool insideSwitch)
{
foreach (var s in statements)
if (ContainsEscapingExit(s, insideLoop, insideSwitch)) return true;
return false;
}
// ContainsEscapingExit / ContainsEscapingExit2 are shared across the suspension-aware emitters and
// live in StatementEmitterBase (the generator, async-generator, and async-function emitters all
// segment a flag-based try body around non-local exits using the same conservative analysis).

#endregion
}
Loading
Loading