diff --git a/src/compiler.ts b/src/compiler.ts index bf4e482b94..5576deabfa 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; @@ -2802,6 +2838,7 @@ export class Compiler extends DiagnosticEmitter { ): ExpressionRef { let module = this.module; let cases = statement.cases; + let labelNode = statement.label; let numCases = cases.length; // Compile the condition (always executes) @@ -2824,6 +2861,9 @@ export class Compiler extends DiagnosticEmitter { let breakIndex = 1; let defaultIndex = -1; let label = outerFlow.pushControlFlowLabel(); + let breakLabel = `break|${label}`; + if (labelNode) outerFlow.addUserLabel(labelNode.text, breakLabel, null, labelNode); + for (let i = 0; i < numCases; ++i) { let case_ = cases[i]; if (case_.isDefault) { @@ -2843,7 +2883,7 @@ export class Compiler extends DiagnosticEmitter { // If there is a default case, break to it, otherwise break out of the switch breaks[breakIndex] = module.br(defaultIndex >= 0 ? `case${defaultIndex}|${label}` - : `break|${label}` + : breakLabel ); // Nest the case blocks in order, to be targeted by the br_if sequence @@ -2859,7 +2899,6 @@ export class Compiler extends DiagnosticEmitter { let innerFlow = outerFlow.fork(/* newBreakContext */ true, /* newContinueContext */ false); if (fallThroughFlow) innerFlow.mergeBranch(fallThroughFlow); this.currentFlow = innerFlow; - let breakLabel = `break|${label}`; innerFlow.breakLabel = breakLabel; let isLast = i == numCases - 1; @@ -2897,6 +2936,7 @@ export class Compiler extends DiagnosticEmitter { currentBlock = module.block(nextLabel, stmts, TypeRef.None); // must be a labeled block } outerFlow.popControlFlowLabel(label); + if (labelNode) outerFlow.removeUserLabel(labelNode.text); // If the switch has a default, we only get past through any breaking flow if (defaultIndex >= 0) { @@ -3208,6 +3248,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 +3262,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;