diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs index 1a998277..a2fa90d8 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs @@ -964,71 +964,9 @@ private static bool ContainsSuspensionInExpr(Expr expr) } } - /// - /// 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). - /// - 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 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 } diff --git a/Compilation/AsyncMoveNextEmitter.Statements.ControlFlow.cs b/Compilation/AsyncMoveNextEmitter.Statements.ControlFlow.cs index fbe2affa..8646bba5 100644 --- a/Compilation/AsyncMoveNextEmitter.Statements.ControlFlow.cs +++ b/Compilation/AsyncMoveNextEmitter.Statements.ControlFlow.cs @@ -39,5 +39,22 @@ protected override void EmitReturn(Stmt.Return r) _il.Emit(OpCodes.Leave, _setResultLabel); } + /// + /// Branch out to (a loop's break/continue label). Inside a real IL + /// exception block a br out is illegal IL (BranchOutOfTry), so use Leave — which exits + /// the block legally and runs its (no-await) finally. ExceptionBlockDepth counts only real + /// blocks opened by , 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 → Br) while a break targeting a loop + /// nested inside a segment stays a legal in-segment Br. Mirrors the generator emitters (#727). + /// + 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) } diff --git a/Compilation/AsyncMoveNextEmitter.Statements.LabeledLoops.cs b/Compilation/AsyncMoveNextEmitter.Statements.LabeledLoops.cs index 54de9271..52eeb405 100644 --- a/Compilation/AsyncMoveNextEmitter.Statements.LabeledLoops.cs +++ b/Compilation/AsyncMoveNextEmitter.Statements.LabeledLoops.cs @@ -113,6 +113,17 @@ private void EmitLabeledWhile(IReadOnlyList labelNames, Stmt.While w, La private void EmitLabeledForOf(IReadOnlyList 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