From b1f614c86b12edaa3b47224b4563445f90cb6088 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 15 Jun 2026 20:38:16 -0700 Subject: [PATCH 1/3] Fix #580: chained label on a for re-runs initializer on continue (compiled) A chain of labels on a loop (a: b: for) used to hand only the innermost label to the loop; the outer label fell into the legacy 'mark continue before the loop' path, so 'continue ' re-ran the for initializer forever (compiled hang). Loop scopes now carry a set of label names: EmitLabeledStatement flattens the chain and parks every label, and the loop drains them all so continue/break to any of them runs the loop's own step. Mirrors the interpreter's _pendingLoopLabels. Adds both-modes tests (continue outer/inner, break outer, triple chain, for-of chain). Async state-machine chained labels tracked in #704. --- ...atorMoveNextEmitter.Statements.TryCatch.cs | 22 ++--- Compilation/CompilationContext.cs | 66 ++++++++----- ...atorMoveNextEmitter.Statements.TryCatch.cs | 14 +-- Compilation/ILEmitter.Statements.cs | 52 ++++++----- Compilation/ILEmitter.cs | 4 +- Compilation/StatementEmitterBase.cs | 64 +++++++++---- .../SharedTests/LabeledStatementTests.cs | 92 ++++++++++++++++++- 7 files changed, 222 insertions(+), 92 deletions(-) diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs index 46cd19e6..545c95d6 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs @@ -28,7 +28,7 @@ private sealed class LoopScope : IExitScope { public required Label BreakLabel; public required Label ContinueLabel; - public string? LabelName; + public IReadOnlyList LabelNames = CompilationContext.NoLabels; // Lazily assigned the first time a break/continue to this loop must run an intervening // finally; identifies that exit in the `<>pendingExit` field. Unset while the loop's @@ -102,11 +102,11 @@ private FieldBuilder GetPendingReturnValueField() => // ---- Loop-scope methods (override the base stack to use `_exitScopes`) ----------------------- protected override void EnterLoop(Label breakLabel, Label continueLabel, string? labelName = null) - // Adopt a pending label (`outer: for (...)`) just like the sync generator emitter does, so a - // labeled `break`/`continue` can resolve this loop as its target. Without the fallback the - // LoopScope registers with LabelName == null and FindLabeledLoopScope never matches, making a - // labeled break/continue a silent no-op — the loop keeps iterating (#586). - => _exitScopes.Add(new LoopScope { BreakLabel = breakLabel, ContinueLabel = continueLabel, LabelName = labelName ?? Ctx.TakePendingLoopLabel() }); + // Adopt any pending labels (`outer: for (...)`, or a chain `a: b: for`) just like the sync + // generator emitter does, so a labeled `break`/`continue` can resolve this loop as its target. + // Without the fallback the LoopScope registers with no labels and FindLabeledLoopScope never + // matches, making a labeled break/continue a silent no-op — the loop keeps iterating (#586). + => _exitScopes.Add(new LoopScope { BreakLabel = breakLabel, ContinueLabel = continueLabel, LabelNames = labelName != null ? new[] { labelName } : Ctx.TakePendingLoopLabels() }); protected override void ExitLoop() { @@ -115,19 +115,19 @@ protected override void ExitLoop() _exitScopes.RemoveAt(_exitScopes.Count - 1); } - protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? CurrentLoop + protected override (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? CurrentLoop { get { var loop = CurrentLoopScope(); - return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelName); + return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelNames); } } - protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? FindLabeledLoop(string labelName) + protected override (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? FindLabeledLoop(string labelName) { var loop = FindLabeledLoopScope(labelName); - return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelName); + return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelNames); } private LoopScope? CurrentLoopScope() @@ -141,7 +141,7 @@ protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? F private LoopScope? FindLabeledLoopScope(string labelName) { for (int i = _exitScopes.Count - 1; i >= 0; i--) - if (_exitScopes[i] is LoopScope ls && ls.LabelName == labelName) + if (_exitScopes[i] is LoopScope ls && ls.LabelNames.Contains(labelName)) return ls; return null; } diff --git a/Compilation/CompilationContext.cs b/Compilation/CompilationContext.cs index 6114e674..25b77592 100644 --- a/Compilation/CompilationContext.cs +++ b/Compilation/CompilationContext.cs @@ -202,27 +202,36 @@ public partial class CompilationContext // Loop and Exception Block Control // ============================================ - // Loop control labels (with optional label name for labeled statements) - public Stack<(Label BreakLabel, Label ContinueLabel, string? LabelName)> LoopLabels { get; } = new(); + // Loop control labels. LabelNames carries every label a labeled break/continue can target — + // usually zero or one, but a chain like `a: b: for` hands the loop both, so `continue a` and + // `continue b` resolve to the same loop. Empty (NoLabels) for an unlabeled loop. + public Stack<(Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)> LoopLabels { get; } = new(); - /// - /// Label name awaiting attachment to the next loop's own break/continue targets. - /// Set by EmitLabeledStatement when the labeled statement is a loop, and consumed - /// by that loop's EnterLoop (here or in an emitter override). This lets - /// continue <label> branch to the loop's real continue point — a for-loop's - /// increment, a while's condition — instead of a point ahead of the loop's initializer, - /// which would re-run it forever (#558). - /// - public string? PendingLoopLabel { get; set; } + /// Shared empty label set for unlabeled loops (avoids per-loop allocation). + public static readonly IReadOnlyList NoLabels = []; + + // Labels parked by EmitLabeledStatement for the loop a chain of them directly wraps. The loop + // drains them all at entry via TakePendingLoopLabels and treats a continue/break to any of them + // as targeting itself, running the loop's own step (a for's increment, a while's re-test) rather + // than restarting it — restarting a `for` would re-run its initializer forever (#558/#580). + private readonly List _pendingLoopLabels = []; + + /// Parks a label for the next loop to adopt. A chain parks several before the loop. + public void AddPendingLoopLabel(string label) => _pendingLoopLabels.Add(label); + + /// Discards any parked labels the next loop didn't drain (defensive cleanup). + public void ClearPendingLoopLabels() => _pendingLoopLabels.Clear(); /// - /// Returns the pending labeled-loop name and clears it, so it attaches to exactly one loop. + /// Returns the labels parked for the loop now being entered, and clears them, so they attach to + /// exactly one loop. /// - public string? TakePendingLoopLabel() + public IReadOnlyList TakePendingLoopLabels() { - var label = PendingLoopLabel; - PendingLoopLabel = null; - return label; + if (_pendingLoopLabels.Count == 0) return NoLabels; + var labels = _pendingLoopLabels.ToArray(); + _pendingLoopLabels.Clear(); + return labels; } // Hoisted array type caches: stack of per-loop dictionaries mapping @@ -340,27 +349,38 @@ public void ClearParameters() public void EnterLoop(Label breakLabel, Label continueLabel, string? labelName = null) { - // An unlabeled EnterLoop call adopts any label parked by an enclosing labeled - // statement, so the loop's own continue/break targets carry the label (#558). - LoopLabels.Push((breakLabel, continueLabel, labelName ?? TakePendingLoopLabel())); + // An explicit label names this loop alone; otherwise the loop adopts whatever an enclosing + // labeled statement parked — a chain hands it several — so its own continue/break targets + // carry every label (#558/#580). + var labels = labelName != null ? new[] { labelName } : TakePendingLoopLabels(); + LoopLabels.Push((breakLabel, continueLabel, labels)); } + /// + /// Enters a loop carrying a pre-collected set of label names. Used where the labels are drained + /// once up front and handed to each of several alternative runtime paths (e.g. for-of's iterator + /// / index-based variants), so every path's break/continue targets resolve no matter which one + /// runs at runtime (#558). + /// + public void EnterLoop(Label breakLabel, Label continueLabel, IReadOnlyList labelNames) + => LoopLabels.Push((breakLabel, continueLabel, labelNames)); + public void ExitLoop() { LoopLabels.Pop(); } - public (Label BreakLabel, Label ContinueLabel, string? LabelName)? CurrentLoop => + public (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? CurrentLoop => LoopLabels.Count > 0 ? LoopLabels.Peek() : null; /// - /// Find a loop label by name (for labeled break/continue). + /// Find a loop scope that carries the given label name (for labeled break/continue). /// - public (Label BreakLabel, Label ContinueLabel, string? LabelName)? FindLabeledLoop(string labelName) + public (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? FindLabeledLoop(string labelName) { foreach (var entry in LoopLabels) { - if (entry.LabelName == labelName) + if (entry.LabelNames.Contains(labelName)) return entry; } return null; diff --git a/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs b/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs index c128822a..712ced5d 100644 --- a/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs +++ b/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs @@ -28,7 +28,7 @@ private sealed class LoopScope : IExitScope { public required Label BreakLabel; public required Label ContinueLabel; - public string? LabelName; + public IReadOnlyList LabelNames = CompilationContext.NoLabels; // Lazily assigned the first time a break/continue to this loop must run an intervening // finally; identifies that exit in the `<>pendingExit` field. Unset while the loop's @@ -133,7 +133,7 @@ private FieldBuilder DefineExceptionPresentField() => // ---- Loop-scope methods (override the base stack to use `_exitScopes`) ----------------------- protected override void EnterLoop(Label breakLabel, Label continueLabel, string? labelName = null) - => _exitScopes.Add(new LoopScope { BreakLabel = breakLabel, ContinueLabel = continueLabel, LabelName = labelName ?? Ctx.TakePendingLoopLabel() }); + => _exitScopes.Add(new LoopScope { BreakLabel = breakLabel, ContinueLabel = continueLabel, LabelNames = labelName != null ? new[] { labelName } : Ctx.TakePendingLoopLabels() }); protected override void ExitLoop() { @@ -142,19 +142,19 @@ protected override void ExitLoop() _exitScopes.RemoveAt(_exitScopes.Count - 1); } - protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? CurrentLoop + protected override (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? CurrentLoop { get { var loop = CurrentLoopScope(); - return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelName); + return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelNames); } } - protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? FindLabeledLoop(string labelName) + protected override (Label BreakLabel, Label ContinueLabel, IReadOnlyList LabelNames)? FindLabeledLoop(string labelName) { var loop = FindLabeledLoopScope(labelName); - return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelName); + return loop == null ? null : (loop.BreakLabel, loop.ContinueLabel, loop.LabelNames); } private LoopScope? CurrentLoopScope() @@ -168,7 +168,7 @@ protected override (Label BreakLabel, Label ContinueLabel, string? LabelName)? F private LoopScope? FindLabeledLoopScope(string labelName) { for (int i = _exitScopes.Count - 1; i >= 0; i--) - if (_exitScopes[i] is LoopScope ls && ls.LabelName == labelName) + if (_exitScopes[i] is LoopScope ls && ls.LabelNames.Contains(labelName)) return ls; return null; } diff --git a/Compilation/ILEmitter.Statements.cs b/Compilation/ILEmitter.Statements.cs index abff9502..e6554632 100644 --- a/Compilation/ILEmitter.Statements.cs +++ b/Compilation/ILEmitter.Statements.cs @@ -488,10 +488,10 @@ protected override void EmitDoWhile(Stmt.DoWhile dw) protected override void EmitForOf(Stmt.ForOf f) { // A for-of emits several alternative runtime paths (iterator protocol, index-based, …), - // each registering its own loop scope. Capture any labeled-loop name once up front and - // hand it to every path, so `continue`/`break