From 66a29fc6df1550c9ae9ce456c212da3a804b8fac Mon Sep 17 00:00:00 2001
From: CountBleck <Mr.YouKnowWhoIAm@protonmail.com>
Date: Sun, 15 Dec 2024 21:56:47 -0800
Subject: [PATCH 1/3] Add support for labeled statements to the parser/AST

This is a prerequisite for supporting labeled breaks/continues. Clearly
unusable labels, such as `x: let foo = 1;` report an error by default,
similar to TS's behavior.
---
 src/ast.ts                  | 40 ++++++++++++++----
 src/diagnosticMessages.json |  1 +
 src/extra/ast.ts            | 17 ++++++++
 src/parser.ts               | 82 ++++++++++++++++++++++++++++---------
 4 files changed, 112 insertions(+), 28 deletions(-)

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/diagnosticMessages.json b/src/diagnosticMessages.json
index 5b0249f9b2..30f493c86f 100644
--- a/src/diagnosticMessages.json
+++ b/src/diagnosticMessages.json
@@ -125,6 +125,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/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 = (<VariableStatement>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 {

From b47dc99c1a295db440d5fa13bccaf283bf1d6fc4 Mon Sep 17 00:00:00 2001
From: CountBleck <Mr.YouKnowWhoIAm@protonmail.com>
Date: Mon, 16 Dec 2024 14:32:43 -0800
Subject: [PATCH 2/3] 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             | 113 +++++++++++++++++++++++++++---------
 src/diagnosticMessages.json |   2 +
 src/flow.ts                 |  45 +++++++++++++-
 3 files changed, 130 insertions(+), 30 deletions(-)

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<ExpressionRef>();
     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<ExpressionRef>();
@@ -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<ExpressionRef>();
     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<ExpressionRef>();
     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 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<string,UserLabels> | null = null;
   /** Scoped local variables. */
   scopedLocals: Map<string,Local> | 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;

From 692b388670c1fe3362666cffb6acb9cf3aef1173 Mon Sep 17 00:00:00 2001
From: CountBleck <Mr.YouKnowWhoIAm@protonmail.com>
Date: Mon, 16 Dec 2024 21:41:29 -0800
Subject: [PATCH 3/3] Add tests for labeled break/continue

---
 .../labeled-break-continue-errors.json        | 10 +++
 .../compiler/labeled-break-continue-errors.ts | 24 ++++++
 tests/compiler/labeled-break-continue.json    |  1 +
 tests/compiler/labeled-break-continue.ts      | 78 +++++++++++++++++++
 tests/parser/labels.ts                        | 26 +++++++
 tests/parser/labels.ts.fixture.ts             | 21 +++++
 6 files changed, 160 insertions(+)
 create mode 100644 tests/compiler/labeled-break-continue-errors.json
 create mode 100644 tests/compiler/labeled-break-continue-errors.ts
 create mode 100644 tests/compiler/labeled-break-continue.json
 create mode 100644 tests/compiler/labeled-break-continue.ts
 create mode 100644 tests/parser/labels.ts
 create mode 100644 tests/parser/labels.ts.fixture.ts

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)