From 60e57da08b3242e8f8c98cd664268aa0acd69151 Mon Sep 17 00:00:00 2001 From: CountBleck Date: Mon, 16 Dec 2024 14:32:43 -0800 Subject: [PATCH] Add support for labeled break/continue This requires an additional field to Flow that maps user-defined statement labels to the internal Binaryen labels passed to module.br(). Thanks to the existing logic to handle unlabeled break/continue, adding support for labeled break/continue is a breeze. Fixes #2889. --- src/compiler.ts | 79 +++++++++++++++++++++++++++---------- src/diagnosticMessages.json | 2 + src/flow.ts | 45 ++++++++++++++++++++- 3 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/compiler.ts b/src/compiler.ts index bf4e482b94..0c47a9cd5a 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -2290,6 +2290,8 @@ export class Compiler extends DiagnosticEmitter { private compileBlockStatement( statement: BlockStatement ): ExpressionRef { + if (statement.label) return this.compileLabeledBlockStatement(statement); + let statements = statement.statements; let outerFlow = this.currentFlow; let innerFlow = outerFlow.fork(); @@ -2301,6 +2303,30 @@ export class Compiler extends DiagnosticEmitter { return this.module.flatten(stmts); } + private compileLabeledBlockStatement( + statement: BlockStatement + ): ExpressionRef { + let statements = statement.statements; + let outerFlow = this.currentFlow; + let innerFlow = outerFlow.fork(); + + let labelNode = assert(statement.label); + let label = innerFlow.pushControlFlowLabel(); + let breakLabel = `block-break|${label}`; + innerFlow.addUserLabel(labelNode.text, breakLabel, null, labelNode); + this.currentFlow = innerFlow; + + let stmts = this.compileStatements(statements); + innerFlow.popControlFlowLabel(label); + innerFlow.removeUserLabel(labelNode.text); + + outerFlow.inherit(innerFlow); + this.currentFlow = outerFlow; + return innerFlow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks) + ? this.module.block(breakLabel, stmts) + : this.module.flatten(stmts); + } + private compileTypeDeclaration(statement: TypeDeclaration): ExpressionRef { let flow = this.currentFlow; let name = statement.name.text; @@ -2324,23 +2350,25 @@ export class Compiler extends DiagnosticEmitter { ): ExpressionRef { let module = this.module; let labelNode = statement.label; + let flow = this.currentFlow; + let breakLabel: string | null = null; if (labelNode) { - this.error( - DiagnosticCode.Not_implemented_0, - labelNode.range, - "Break label" - ); - return module.unreachable(); + const userLabel = flow.getUserLabel(labelNode.text); + if (userLabel) breakLabel = userLabel.breakLabel; + } else { + breakLabel = flow.breakLabel; } - let flow = this.currentFlow; - let breakLabel = flow.breakLabel; + if (breakLabel == null) { this.error( - DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement, + labelNode + ? DiagnosticCode.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement + : DiagnosticCode.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement, statement.range ); return module.unreachable(); } + flow.set(FlowFlags.Breaks); return module.br(breakLabel); } @@ -2349,25 +2377,27 @@ export class Compiler extends DiagnosticEmitter { statement: ContinueStatement ): ExpressionRef { let module = this.module; - let label = statement.label; - if (label) { - this.error( - DiagnosticCode.Not_implemented_0, - label.range, - "Continue label" - ); - return module.unreachable(); + let labelNode = statement.label; + let flow = this.currentFlow; + let continueLabel: string | null = null; + if (labelNode) { + const userLabel = flow.getUserLabel(labelNode.text); + if (userLabel) continueLabel = userLabel.continueLabel; + } else { + continueLabel = flow.continueLabel; } + // Check if 'continue' is allowed here - let flow = this.currentFlow; - let continueLabel = flow.continueLabel; if (continueLabel == null) { this.error( - DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement, + labelNode + ? DiagnosticCode.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement + : DiagnosticCode.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement, statement.range ); return module.unreachable(); } + flow.set(FlowFlags.Continues | FlowFlags.Terminates); return module.br(continueLabel); } @@ -2409,6 +2439,8 @@ export class Compiler extends DiagnosticEmitter { let continueLabel = `do-continue|${label}`; flow.continueLabel = continueLabel; let loopLabel = `do-loop|${label}`; + let labelNode = statement.label; + if (labelNode) flow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode); this.currentFlow = flow; let bodyStmts = new Array(); let body = statement.body; @@ -2418,6 +2450,7 @@ export class Compiler extends DiagnosticEmitter { bodyStmts.push(this.compileStatement(body)); } flow.popControlFlowLabel(label); + if (labelNode) flow.removeUserLabel(labelNode.text); let possiblyContinues = flow.isAny(FlowFlags.Continues | FlowFlags.ConditionallyContinues); let possiblyBreaks = flow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks); @@ -2573,6 +2606,8 @@ export class Compiler extends DiagnosticEmitter { bodyFlow.breakLabel = breakLabel; let continueLabel = `for-continue|${label}`; bodyFlow.continueLabel = continueLabel; + let labelNode = statement.label; + if (labelNode) bodyFlow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode); let loopLabel = `for-loop|${label}`; this.currentFlow = bodyFlow; let bodyStmts = new Array(); @@ -2583,6 +2618,7 @@ export class Compiler extends DiagnosticEmitter { bodyStmts.push(this.compileStatement(body)); } bodyFlow.popControlFlowLabel(label); + if (labelNode) bodyFlow.removeUserLabel(labelNode.text); bodyFlow.breakLabel = null; bodyFlow.continueLabel = null; @@ -3208,6 +3244,8 @@ export class Compiler extends DiagnosticEmitter { thenFlow.breakLabel = breakLabel; let continueLabel = `while-continue|${label}`; thenFlow.continueLabel = continueLabel; + let labelNode = statement.label; + if (labelNode) thenFlow.addUserLabel(labelNode.text, breakLabel, continueLabel, labelNode); this.currentFlow = thenFlow; let bodyStmts = new Array(); let body = statement.body; @@ -3220,6 +3258,7 @@ export class Compiler extends DiagnosticEmitter { module.br(continueLabel) ); thenFlow.popControlFlowLabel(label); + if (labelNode) thenFlow.removeUserLabel(labelNode.text); let possiblyContinues = thenFlow.isAny(FlowFlags.Continues | FlowFlags.ConditionallyContinues); let possiblyBreaks = thenFlow.isAny(FlowFlags.Breaks | FlowFlags.ConditionallyBreaks); diff --git a/src/diagnosticMessages.json b/src/diagnosticMessages.json index 30f493c86f..c2a6855d83 100644 --- a/src/diagnosticMessages.json +++ b/src/diagnosticMessages.json @@ -94,6 +94,8 @@ "Type expected.": 1110, "A 'default' clause cannot appear more than once in a 'switch' statement.": 1113, "Duplicate label '{0}'.": 1114, + "A 'continue' statement can only jump to a label of an enclosing iteration statement.": 1115, + "A 'break' statement can only jump to a label of an enclosing statement": 1116, "An export assignment cannot have modifiers.": 1120, "Octal literals are not allowed in strict mode.": 1121, "Digit expected.": 1124, diff --git a/src/flow.ts b/src/flow.ts index d481a61fc9..475c10e185 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -199,6 +199,15 @@ export const enum ConditionKind { False } +class UserLabels { + constructor( + /** The label we break to when encountering a break statement. */ + readonly breakLabel: string, + /** The label we break to when encountering a continue statement. */ + readonly continueLabel: string | null + ) {} +} + /** A control flow evaluator. */ export class Flow { @@ -245,10 +254,12 @@ export class Flow { outer: Flow | null = null; /** Flow flags indicating specific conditions. */ flags: FlowFlags = FlowFlags.None; - /** The label we break to when encountering a continue statement. */ + /** The label we break to when encountering an unlabeled continue statement. */ continueLabel: string | null = null; - /** The label we break to when encountering a break statement. */ + /** The label we break to when encountering an unlabeled break statement. */ breakLabel: string | null = null; + /** Map of user-declared statement label names to internal label names */ + userLabelMap: Map | null = null; /** Scoped local variables. */ scopedLocals: Map | null = null; /** Scoped type alias. */ @@ -351,6 +362,9 @@ export class Flow { } else { branch.continueLabel = this.continueLabel; } + let userLabelMap = this.userLabelMap; + if (userLabelMap) userLabelMap = cloneMap(userLabelMap); + branch.userLabelMap = userLabelMap; branch.localFlags = this.localFlags.slice(); if (this.sourceFunction.is(CommonFlags.Constructor)) { let thisFieldFlags = assert(this.thisFieldFlags); @@ -447,6 +461,33 @@ export class Flow { return local; } + + /** Gets the internal labels associated with a user-declared label name. */ + getUserLabel(name: string): UserLabels | null { + const userLabelMap = this.userLabelMap; + if (userLabelMap && userLabelMap.has(name)) return assert(userLabelMap.get(name)); + return null; + } + + /** Associates a user-declared label name with internal labels. */ + addUserLabel(name: string, breakLabel: string, continueLabel: string | null, declarationNode: Node): void { + let userLabelMap = this.userLabelMap; + if (!userLabelMap) { + this.userLabelMap = userLabelMap = new Map(); + } else if (userLabelMap.has(name)) { + this.program.error(DiagnosticCode.Duplicate_label_0, declarationNode.range, name); + } + + userLabelMap.set(name, new UserLabels(breakLabel, continueLabel)); + } + + /** Remove a user-declared label name. */ + removeUserLabel(name: string): void { + let userLabelMap = assert(this.userLabelMap); + assert(userLabelMap.has(name)); + userLabelMap.delete(name); + } + /** Gets the scoped local of the specified name. */ getScopedLocal(name: string): Local | null { let scopedLocals = this.scopedLocals;