diff --git a/src/ast.ts b/src/ast.ts index da2271ef3d..34bc653eb3 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -440,9 +440,10 @@ export abstract class Node { static createBlockStatement( statements: Statement[], + label: IdentifierExpression | null, range: Range ): BlockStatement { - return new BlockStatement(statements, range); + return new BlockStatement(statements, label, range); } static createBreakStatement( @@ -475,9 +476,10 @@ export abstract class Node { static createDoStatement( body: Statement, condition: Expression, + label: IdentifierExpression | null, range: Range ): DoStatement { - return new DoStatement(body, condition, range); + return new DoStatement(body, condition, label, range); } static createEmptyStatement( @@ -548,9 +550,10 @@ export abstract class Node { condition: Expression, ifTrue: Statement, ifFalse: Statement | null, + label: IdentifierExpression | null, range: Range ): IfStatement { - return new IfStatement(condition, ifTrue, ifFalse, range); + return new IfStatement(condition, ifTrue, ifFalse, label, range); } static createImportStatement( @@ -607,18 +610,20 @@ export abstract class Node { condition: Expression | null, incrementor: Expression | null, body: Statement, + label: IdentifierExpression | null, range: Range ): ForStatement { - return new ForStatement(initializer, condition, incrementor, body, range); + return new ForStatement(initializer, condition, incrementor, body, label, range); } static createForOfStatement( variable: Statement, iterable: Expression, body: Statement, + label: IdentifierExpression | null, range: Range ): ForOfStatement { - return new ForOfStatement(variable, iterable, body, range); + return new ForOfStatement(variable, iterable, body, label, range); } static createFunctionDeclaration( @@ -675,9 +680,10 @@ export abstract class Node { static createSwitchStatement( condition: Expression, cases: SwitchCase[], + label: IdentifierExpression | null, range: Range ): SwitchStatement { - return new SwitchStatement(condition, cases, range); + return new SwitchStatement(condition, cases, label, range); } static createSwitchCase( @@ -700,9 +706,10 @@ export abstract class Node { catchVariable: IdentifierExpression | null, catchStatements: Statement[] | null, finallyStatements: Statement[] | null, + label: IdentifierExpression | null, range: Range ): TryStatement { - return new TryStatement(bodyStatements, catchVariable, catchStatements, finallyStatements, range); + return new TryStatement(bodyStatements, catchVariable, catchStatements, finallyStatements, label, range); } static createTypeDeclaration( @@ -753,9 +760,10 @@ export abstract class Node { static createWhileStatement( condition: Expression, statement: Statement, + label: IdentifierExpression | null, range: Range ): WhileStatement { - return new WhileStatement(condition, statement, range); + return new WhileStatement(condition, statement, label, range); } /** Tests if this node is a literal of the specified kind. */ @@ -1788,6 +1796,8 @@ export class BlockStatement extends Statement { constructor( /** Contained statements. */ public statements: Statement[], + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -1858,6 +1868,8 @@ export class DoStatement extends Statement { public body: Statement, /** Condition when to repeat. */ public condition: Expression, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2022,6 +2034,8 @@ export class ForStatement extends Statement { public incrementor: Expression | null, /** Body statement being looped over. */ public body: Statement, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2038,6 +2052,8 @@ export class ForOfStatement extends Statement { public iterable: Expression, /** Body statement being looped over. */ public body: Statement, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2108,6 +2124,8 @@ export class IfStatement extends Statement { public ifTrue: Statement, /** Statement executed when condition is `false`. */ public ifFalse: Statement | null, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2258,6 +2276,8 @@ export class SwitchStatement extends Statement { public condition: Expression, /** Contained cases. */ public cases: SwitchCase[], + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2288,6 +2308,8 @@ export class TryStatement extends Statement { public catchStatements: Statement[] | null, /** Statements being executed afterwards, if a `finally` clause is present. */ public finallyStatements: Statement[] | null, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { @@ -2382,6 +2404,8 @@ export class WhileStatement extends Statement { public condition: Expression, /** Body statement being looped over. */ public body: Statement, + /** Label, if any. */ + public label: IdentifierExpression | null, /** Source range. */ range: Range ) { diff --git a/src/compiler.ts b/src/compiler.ts index bf4e482b94..00aa4918ab 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; @@ -2683,17 +2719,27 @@ export class Compiler extends DiagnosticEmitter { ); let condKind = this.evaluateCondition(condExprTrueish); + let flow = this.currentFlow; + let label = -1; + let labelNode = statement.label; + let breakLabel: string | null = null; + if (labelNode) { + label = flow.pushControlFlowLabel(); + breakLabel = `if-break|${label}`; + flow.addUserLabel(labelNode.text, breakLabel, null, labelNode); + } + // Shortcut if the condition is constant switch (condKind) { case ConditionKind.True: { - return module.block(null, [ + return module.block(breakLabel, [ module.drop(condExprTrueish), this.compileStatement(ifTrue) ]); } case ConditionKind.False: { return ifFalse - ? module.block(null, [ + ? module.block(breakLabel, [ module.drop(condExprTrueish), this.compileStatement(ifFalse) ]) @@ -2703,8 +2749,6 @@ export class Compiler extends DiagnosticEmitter { // From here on condition is always unknown - let flow = this.currentFlow; - // Compile ifTrue assuming the condition turned out true let thenStmts = new Array(); let thenFlow = flow.forkThen(condExpr); @@ -2717,6 +2761,7 @@ export class Compiler extends DiagnosticEmitter { this.currentFlow = flow; // Compile ifFalse assuming the condition turned out false, if present + let expr: ExpressionRef; let elseFlow = flow.forkElse(condExpr); if (ifFalse) { this.currentFlow = elseFlow; @@ -2728,7 +2773,7 @@ export class Compiler extends DiagnosticEmitter { } flow.inheritAlternatives(thenFlow, elseFlow); // terminates if both do this.currentFlow = flow; - return module.if(condExprTrueish, + expr = module.if(condExprTrueish, module.flatten(thenStmts), module.flatten(elseStmts) ); @@ -2742,10 +2787,15 @@ export class Compiler extends DiagnosticEmitter { flow.inheritAlternatives(thenFlow, elseFlow); } this.currentFlow = flow; - return module.if(condExprTrueish, + expr = module.if(condExprTrueish, module.flatten(thenStmts) ); } + + if (!labelNode) return expr; + flow.popControlFlowLabel(label); + flow.removeUserLabel(labelNode.text); + return module.block(breakLabel, [expr]); } private compileReturnStatement( @@ -2802,6 +2852,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 +2875,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 +2897,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 +2913,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 +2950,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 +3262,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 +3276,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 5b0249f9b2..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, @@ -125,6 +127,7 @@ "A class may only extend another class.": 1311, "A parameter property cannot be declared using a rest parameter.": 1317, "A default export can only be used in a module.": 1319, + "A label is not allowed here.": 1344, "An expression of type '{0}' cannot be tested for truthiness.": 1345, "An identifier or keyword cannot immediately follow a numeric literal.": 1351, diff --git a/src/extra/ast.ts b/src/extra/ast.ts index ebe9217f90..5ca23bf79e 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -801,6 +801,7 @@ export class ASTBuilder { let sb = this.sb; let statements = node.statements; let numStatements = statements.length; + this.visitLabel(node.label); if (numStatements) { sb.push("{\n"); let indentLevel = ++this.indentLevel; @@ -815,6 +816,15 @@ export class ASTBuilder { } } + private visitLabel(label: IdentifierExpression | null) { + if (!label) return; + + let sb = this.sb; + this.visitIdentifierExpression(label); + sb.push(":\n"); + indent(sb, this.indentLevel); + } + visitBreakStatement(node: BreakStatement): void { let label = node.label; if (label) { @@ -908,6 +918,7 @@ export class ASTBuilder { visitDoStatement(node: DoStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("do "); this.visitNode(node.body); if (node.body.kind == NodeKind.Block) { @@ -1070,6 +1081,7 @@ export class ASTBuilder { visitForStatement(node: ForStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("for ("); let initializer = node.initializer; if (initializer) { @@ -1095,6 +1107,7 @@ export class ASTBuilder { visitForOfStatement(node: ForOfStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("for ("); this.visitNode(node.variable); sb.push(" of "); @@ -1205,6 +1218,7 @@ export class ASTBuilder { visitIfStatement(node: IfStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("if ("); this.visitNode(node.condition); sb.push(") "); @@ -1397,6 +1411,7 @@ export class ASTBuilder { visitSwitchStatement(node: SwitchStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("switch ("); this.visitNode(node.condition); sb.push(") {\n"); @@ -1418,6 +1433,7 @@ export class ASTBuilder { visitTryStatement(node: TryStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("try {\n"); let indentLevel = ++this.indentLevel; let bodyStatements = node.bodyStatements; @@ -1528,6 +1544,7 @@ export class ASTBuilder { visitWhileStatement(node: WhileStatement): void { let sb = this.sb; + this.visitLabel(node.label); sb.push("while ("); this.visitNode(node.condition); let body = node.body; 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; diff --git a/src/parser.ts b/src/parser.ts index 7c69843973..640d913913 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2899,7 +2899,37 @@ export class Parser extends DiagnosticEmitter { let state = tn.mark(); let token = tn.next(); + let label: IdentifierExpression | null = null; let statement: Statement | null = null; + + // Detect labeled statements + if (token == Token.Identifier) { + const preIdentifierState = tn.mark(); + const identifier = tn.readIdentifier(); + const range = tn.range(); + + if (tn.skip(Token.Colon)) { + label = Node.createIdentifierExpression(identifier, range); + token = tn.next(); + + switch (token) { + case Token.Do: + case Token.For: + case Token.If: + case Token.OpenBrace: + case Token.Switch: + case Token.Try: + case Token.While: + // Do nothing + break; + default: + this.error(DiagnosticCode.A_label_is_not_allowed_here, range); + } + } else { + tn.reset(preIdentifierState); + } + } + switch (token) { case Token.Break: { statement = this.parseBreak(tn); @@ -2914,15 +2944,15 @@ export class Parser extends DiagnosticEmitter { break; } case Token.Do: { - statement = this.parseDoStatement(tn); + statement = this.parseDoStatement(tn, label); break; } case Token.For: { - statement = this.parseForStatement(tn); + statement = this.parseForStatement(tn, label); break; } case Token.If: { - statement = this.parseIfStatement(tn); + statement = this.parseIfStatement(tn, label); break; } case Token.Let: { @@ -2934,7 +2964,7 @@ export class Parser extends DiagnosticEmitter { break; } case Token.OpenBrace: { - statement = this.parseBlockStatement(tn, topLevel); + statement = this.parseBlockStatement(tn, topLevel, label); break; } case Token.Return: { @@ -2951,7 +2981,7 @@ export class Parser extends DiagnosticEmitter { return Node.createEmptyStatement(tn.range(tn.tokenPos)); } case Token.Switch: { - statement = this.parseSwitchStatement(tn); + statement = this.parseSwitchStatement(tn, label); break; } case Token.Throw: { @@ -2959,7 +2989,7 @@ export class Parser extends DiagnosticEmitter { break; } case Token.Try: { - statement = this.parseTryStatement(tn); + statement = this.parseTryStatement(tn, label); break; } case Token.Void: { @@ -2967,7 +2997,7 @@ export class Parser extends DiagnosticEmitter { break; } case Token.While: { - statement = this.parseWhileStatement(tn); + statement = this.parseWhileStatement(tn, label); break; } case Token.Type: { // also identifier @@ -2994,7 +3024,8 @@ export class Parser extends DiagnosticEmitter { parseBlockStatement( tn: Tokenizer, - topLevel: bool + topLevel: bool, + label: IdentifierExpression | null = null ): BlockStatement | null { // at '{': Statement* '}' ';'? @@ -3013,7 +3044,7 @@ export class Parser extends DiagnosticEmitter { statements.push(statement); } } - let ret = Node.createBlockStatement(statements, tn.range(startPos, tn.pos)); + let ret = Node.createBlockStatement(statements, label, tn.range(startPos, tn.pos)); if (topLevel) tn.skip(Token.Semicolon); return ret; } @@ -3051,7 +3082,8 @@ export class Parser extends DiagnosticEmitter { } parseDoStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null ): DoStatement | null { // at 'do': Statement 'while' '(' Expression ')' ';'? @@ -3067,7 +3099,7 @@ export class Parser extends DiagnosticEmitter { if (!condition) return null; if (tn.skip(Token.CloseParen)) { - let ret = Node.createDoStatement(statement, condition, tn.range(startPos, tn.pos)); + let ret = Node.createDoStatement(statement, condition, label, tn.range(startPos, tn.pos)); tn.skip(Token.Semicolon); return ret; } else { @@ -3106,7 +3138,8 @@ export class Parser extends DiagnosticEmitter { } parseForStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null ): Statement | null { // at 'for': '(' Statement? Expression? ';' Expression? ')' Statement @@ -3139,7 +3172,7 @@ export class Parser extends DiagnosticEmitter { ); return null; } - return this.parseForOfStatement(tn, startPos, initializer); + return this.parseForOfStatement(tn, startPos, initializer, label); } if (initializer.kind == NodeKind.Variable) { let declarations = (initializer).declarations; @@ -3153,7 +3186,7 @@ export class Parser extends DiagnosticEmitter { ); // recoverable } } - return this.parseForOfStatement(tn, startPos, initializer); + return this.parseForOfStatement(tn, startPos, initializer, label); } this.error( DiagnosticCode.Identifier_expected, @@ -3215,6 +3248,7 @@ export class Parser extends DiagnosticEmitter { : null, incrementor, statement, + label, tn.range(startPos, tn.pos) ); @@ -3243,6 +3277,7 @@ export class Parser extends DiagnosticEmitter { tn: Tokenizer, startPos: i32, variable: Statement, + label: IdentifierExpression | null ): ForOfStatement | null { // at 'of': Expression ')' Statement @@ -3265,12 +3300,14 @@ export class Parser extends DiagnosticEmitter { variable, iterable, statement, + label, tn.range(startPos, tn.pos) ); } parseIfStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null ): IfStatement | null { // at 'if': '(' Expression ')' Statement ('else' Statement)? @@ -3291,6 +3328,7 @@ export class Parser extends DiagnosticEmitter { condition, statement, elseStatement, + label, tn.range(startPos, tn.pos) ); } else { @@ -3309,7 +3347,8 @@ export class Parser extends DiagnosticEmitter { } parseSwitchStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null ): SwitchStatement | null { // at 'switch': '(' Expression ')' '{' SwitchCase* '}' ';'? @@ -3326,7 +3365,7 @@ export class Parser extends DiagnosticEmitter { if (!switchCase) return null; switchCases.push(switchCase); } - let ret = Node.createSwitchStatement(condition, switchCases, tn.range(startPos, tn.pos)); + let ret = Node.createSwitchStatement(condition, switchCases, label, tn.range(startPos, tn.pos)); tn.skip(Token.Semicolon); return ret; } else { @@ -3427,7 +3466,8 @@ export class Parser extends DiagnosticEmitter { } parseTryStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null = null, ): TryStatement | null { // at 'try': @@ -3511,6 +3551,7 @@ export class Parser extends DiagnosticEmitter { catchVariable, catchStatements, finallyStatements, + label, tn.range(startPos, tn.pos) ); tn.skip(Token.Semicolon); @@ -3609,7 +3650,8 @@ export class Parser extends DiagnosticEmitter { } parseWhileStatement( - tn: Tokenizer + tn: Tokenizer, + label: IdentifierExpression | null ): WhileStatement | null { // at 'while': '(' Expression ')' Statement ';'? @@ -3621,7 +3663,7 @@ export class Parser extends DiagnosticEmitter { if (tn.skip(Token.CloseParen)) { let statement = this.parseStatement(tn); if (!statement) return null; - let ret = Node.createWhileStatement(expression, statement, tn.range(startPos, tn.pos)); + let ret = Node.createWhileStatement(expression, statement, label, tn.range(startPos, tn.pos)); tn.skip(Token.Semicolon); return ret; } else { diff --git a/tests/compiler/labeled-break-continue-errors.json b/tests/compiler/labeled-break-continue-errors.json new file mode 100644 index 0000000000..bc85e1c85c --- /dev/null +++ b/tests/compiler/labeled-break-continue-errors.json @@ -0,0 +1,10 @@ +{ + "stderr": [ + "TS1116: A 'break' statement can only jump to a label of an enclosing statement", + "TS1114: Duplicate label 'duplicate'.", + "TS1115: A 'continue' statement can only jump to a label of an enclosing iteration statement.", + "TS1115: A 'continue' statement can only jump to a label of an enclosing iteration statement.", + "TS1115: A 'continue' statement can only jump to a label of an enclosing iteration statement.", + "EOF" + ] +} \ No newline at end of file diff --git a/tests/compiler/labeled-break-continue-errors.ts b/tests/compiler/labeled-break-continue-errors.ts new file mode 100644 index 0000000000..c6ae629ba6 --- /dev/null +++ b/tests/compiler/labeled-break-continue-errors.ts @@ -0,0 +1,24 @@ +{ + break nonexistent; +} + +duplicate: +{ + duplicate: {} +} + +for (let i = 0; i < 3; i++) { + continue nonexistent; +} + +labelA: +{ + continue labelA; +} + +labelB: +switch (0) { + case 0: continue labelB; +} + +ERROR("EOF") \ No newline at end of file diff --git a/tests/compiler/labeled-break-continue.json b/tests/compiler/labeled-break-continue.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/tests/compiler/labeled-break-continue.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/compiler/labeled-break-continue.ts b/tests/compiler/labeled-break-continue.ts new file mode 100644 index 0000000000..15120ce250 --- /dev/null +++ b/tests/compiler/labeled-break-continue.ts @@ -0,0 +1,78 @@ +function tests(): string[] { + const results: string[] = []; + + label: + for (let i = 0; i < 6; i++) { + if (i == 2) { + results.push("continue!"); + continue label; + } + + if (i == 4) { + results.push("break!"); + break label; + } + + results.push(`first loop ${i}`); + } + + sweetch: + switch (1) { + case 1: + for (let i = 0; i < 6; i++) { + results.push(`second ${i}`); + if (i == 3) break sweetch; + } + results.push("skipped"); + } + + escape: + { + results.push("hi"); + break escape; + results.push("skipped"); + } + + outer: + for (let i = 0; i < 5; i++) { + exit: + if (i % 2 == 0) { + for (let j = 0; j < 4; j++) { + if (i == j) continue outer; + else if (j > i) break exit; + + results.push(`did ${i} ${j}`); + } + } + + results.push(`reached end of ${i}`); + } + + return results; +} + +const results = tests(); +const expected = [ + "first loop 0", + "first loop 1", + "continue!", + "first loop 3", + "break!", + "second 0", + "second 1", + "second 2", + "second 3", + "hi", + "reached end of 1", + "did 2 0", + "did 2 1", + "reached end of 3", + "did 4 0", + "did 4 1", + "did 4 2", + "did 4 3", + "reached end of 4" +]; + +assert(results.length == expected.length); +for (let i = 0; i < expected.length; i++) assert(results[i] == expected[i]); \ No newline at end of file diff --git a/tests/parser/labels.ts b/tests/parser/labels.ts new file mode 100644 index 0000000000..23dec9a7e1 --- /dev/null +++ b/tests/parser/labels.ts @@ -0,0 +1,26 @@ +labelA: +{ + a; + b; +} + +labelB: +for (let i = 0; i < 3; i++) {} + +labelC: +for (const x of y) {} + +labelD: +do {} while (0) + +labelE: +while (0) {} + +labelF: +try {} catch (e) {} + +labelG: +if (0) {} else {} + +labelH: // ERROR 1344: "A label is not allowed here." +let x = 123; \ No newline at end of file diff --git a/tests/parser/labels.ts.fixture.ts b/tests/parser/labels.ts.fixture.ts new file mode 100644 index 0000000000..640e7dfe60 --- /dev/null +++ b/tests/parser/labels.ts.fixture.ts @@ -0,0 +1,21 @@ +labelA: +{ + a; + b; +} +labelB: +for (let i = 0; i < 3; i++) {} +labelC: +for (const x of y) {} +labelD: +do {} while (0); +labelE: +while (0) {} +labelF: +try { +} catch (e) { +} +labelG: + if (0) {} else {} +let x = 123; +// ERROR 1344: "A label is not allowed here." in labels.ts(25,1+6)