Repository: rokucommunity/brighterscript
Pull request #697 (2022-09-27 15:50:05)

Syntax and transpile support for continue statement
Roku OS 11.5 introduced the `continue` statement. This PR adds the necessary parsing and transpiling to support using that feature as defined by Roku. 

This does not address backwards-compatibile transpiling (i.e. transpile to labels and `goto` expressions). That effort is still ongoing in #489 and will be addressed at a future time.

arturocuya: Does the new `Continue` statement support plugin annotations?
TwitchBronBron: @arturocuya do you mean like this? If so, then no. I believe annotations are currently only applicable to the higher-level things like namespaces and functions. 
```bs
while true 
    @YourCustomAnnotation
    continue while
end while
```

TwitchBronBron: Oh, well if annotations work with `throw`, then they should also work for `continue`, as both are "statements". 

Patch:
diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index f87cea00..e5eed8ec 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -572,8 +572,8 @@ export let DiagnosticMessages = {
         code: 1108,
         severity: DiagnosticSeverity.Error
     }),
-    expectedToken: (tokenKind: string) => ({
-        message: `Expected '${tokenKind}'`,
+    expectedToken: (...tokenKinds: string[]) => ({
+        message: `Expected token '${tokenKinds.join(`' or '`)}'`,
         code: 1109,
         severity: DiagnosticSeverity.Error
     }),
@@ -699,6 +699,11 @@ export let DiagnosticMessages = {
         message: `Expected directory depth no larger than 7, but found ${numberOfParentDirectories}.`,
         code: 1134,
         severity: DiagnosticSeverity.Error
+    }),
+    illegalContinueStatement: () => ({
+        message: `Continue statement must be contained within a loop statement`,
+        code: 1135,
+        severity: DiagnosticSeverity.Error
     })
 };
 
diff --git a/src/astUtils/reflection.ts b/src/astUtils/reflection.ts
index 00794af4..48ec3dc0 100644
--- a/src/astUtils/reflection.ts
+++ b/src/astUtils/reflection.ts
@@ -1,4 +1,4 @@
-import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, MethodStatement, FieldStatement, ConstStatement } from '../parser/Statement';
+import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassFieldStatement, ClassMethodStatement, ClassStatement, InterfaceFieldStatement, InterfaceMethodStatement, InterfaceStatement, EnumStatement, EnumMemberStatement, TryCatchStatement, CatchStatement, MethodStatement, FieldStatement, ConstStatement, ContinueStatement } from '../parser/Statement';
 import type { LiteralExpression, BinaryExpression, CallExpression, FunctionExpression, NamespacedVariableNameExpression, DottedGetExpression, XmlAttributeGetExpression, IndexedGetExpression, GroupingExpression, EscapedCharCodeLiteralExpression, ArrayLiteralExpression, AALiteralExpression, UnaryExpression, VariableExpression, SourceLiteralExpression, NewExpression, CallfuncExpression, TemplateStringQuasiExpression, TemplateStringExpression, TaggedTemplateStringExpression, AnnotationExpression, FunctionParameterExpression, AAMemberExpression } from '../parser/Expression';
 import type { BrsFile } from '../files/BrsFile';
 import type { XmlFile } from '../files/XmlFile';
@@ -162,6 +162,9 @@ export function isEnumMemberStatement(element: AstNode | undefined): element is
 export function isConstStatement(element: AstNode | undefined): element is ConstStatement {
     return element?.constructor.name === 'ConstStatement';
 }
+export function isContinueStatement(element: AstNode | undefined): element is ContinueStatement {
+    return element?.constructor.name === 'ContinueStatement';
+}
 export function isTryCatchStatement(element: AstNode | undefined): element is TryCatchStatement {
     return element?.constructor.name === 'TryCatchStatement';
 }
diff --git a/src/astUtils/visitors.ts b/src/astUtils/visitors.ts
index 4b0768cd..a1dea6ca 100644
--- a/src/astUtils/visitors.ts
+++ b/src/astUtils/visitors.ts
@@ -1,6 +1,6 @@
 /* eslint-disable no-bitwise */
 import type { CancellationToken } from 'vscode-languageserver';
-import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement } from '../parser/Statement';
+import type { Body, AssignmentStatement, Block, ExpressionStatement, CommentStatement, ExitForStatement, ExitWhileStatement, FunctionStatement, IfStatement, IncrementStatement, PrintStatement, GotoStatement, LabelStatement, ReturnStatement, EndStatement, StopStatement, ForStatement, ForEachStatement, WhileStatement, DottedSetStatement, IndexedSetStatement, LibraryStatement, NamespaceStatement, ImportStatement, ClassStatement, ClassMethodStatement, ClassFieldStatement, EnumStatement, EnumMemberStatement, DimStatement, TryCatchStatement, CatchStatement, ThrowStatement, InterfaceStatement, InterfaceFieldStatement, InterfaceMethodStatement, FieldStatement, MethodStatement, ConstStatement, ContinueStatement } from '../parser/Statement';
 import type { AALiteralExpression, AAMemberExpression, AnnotationExpression, ArrayLiteralExpression, BinaryExpression, CallExpression, CallfuncExpression, DottedGetExpression, EscapedCharCodeLiteralExpression, FunctionExpression, FunctionParameterExpression, GroupingExpression, IndexedGetExpression, LiteralExpression, NamespacedVariableNameExpression, NewExpression, NullCoalescingExpression, RegexLiteralExpression, SourceLiteralExpression, TaggedTemplateStringExpression, TemplateStringExpression, TemplateStringQuasiExpression, TernaryExpression, UnaryExpression, VariableExpression, XmlAttributeGetExpression } from '../parser/Expression';
 import { isExpression, isStatement } from './reflection';
 import type { AstEditor } from './AstEditor';
@@ -127,6 +127,7 @@ export function createVisitor(
          * @deprecated use `FieldStatement`
          */
         ClassFieldStatement?: (statement: ClassFieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
+        ContinueStatement?: (statement: ContinueStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
         MethodStatement?: (statement: MethodStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
         FieldStatement?: (statement: FieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
         TryCatchStatement?: (statement: TryCatchStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts
index b9ead406..1be5816d 100644
--- a/src/bscPlugin/validation/BrsFileValidator.ts
+++ b/src/bscPlugin/validation/BrsFileValidator.ts
@@ -1,4 +1,4 @@
-import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement } from '../../astUtils/reflection';
+import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isForEachStatement, isForStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement, isWhileStatement } from '../../astUtils/reflection';
 import { createVisitor, WalkMode } from '../../astUtils/visitors';
 import { DiagnosticMessages } from '../../DiagnosticMessages';
 import type { BrsFile } from '../../files/BrsFile';
@@ -7,7 +7,7 @@ import { TokenKind } from '../../lexer/TokenKind';
 import type { AstNode } from '../../parser/AstNode';
 import type { LiteralExpression } from '../../parser/Expression';
 import { ParseMode } from '../../parser/Parser';
-import type { EnumMemberStatement, EnumStatement, ImportStatement, LibraryStatement } from '../../parser/Statement';
+import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement';
 import { DynamicType } from '../../types/DynamicType';
 import util from '../../util';
 
@@ -146,6 +146,9 @@ export class BrsFileValidator {
                 if (node.identifier) {
                     node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
                 }
+            },
+            ContinueStatement: (node) => {
+                this.validateContinueStatement(node);
             }
         });
 
@@ -299,4 +302,39 @@ export class BrsFileValidator {
             }
         }
     }
+
+    private validateContinueStatement(statement: ContinueStatement) {
+        const validateLoopTypeMatch = (loopType: TokenKind) => {
+            //coerce ForEach to For
+            loopType = loopType === TokenKind.ForEach ? TokenKind.For : loopType;
+
+            if (loopType?.toLowerCase() !== statement.tokens.loopType.text?.toLowerCase()) {
+                this.event.file.addDiagnostic({
+                    range: statement.tokens.loopType.range,
+                    ...DiagnosticMessages.expectedToken(loopType)
+                });
+            }
+        };
+
+        //find the parent loop statement
+        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
+            if (isWhileStatement(node)) {
+                validateLoopTypeMatch(node.tokens.while.kind);
+                return true;
+            } else if (isForStatement(node)) {
+                validateLoopTypeMatch(node.forToken.kind);
+                return true;
+            } else if (isForEachStatement(node)) {
+                validateLoopTypeMatch(node.tokens.forEach.kind);
+                return true;
+            }
+        });
+        //flag continue statements found outside of a loop
+        if (!parent) {
+            this.event.file.addDiagnostic({
+                range: statement.range,
+                ...DiagnosticMessages.illegalContinueStatement()
+            });
+        }
+    }
 }
diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts
index 7e7c2686..33e525d7 100644
--- a/src/lexer/Lexer.spec.ts
+++ b/src/lexer/Lexer.spec.ts
@@ -1368,6 +1368,15 @@ describe('lexer', () => {
             );
         });
     });
+
+    it('detects "continue" as a keyword', () => {
+        expect(
+            Lexer.scan('continue').tokens.map(x => x.kind)
+        ).to.eql([
+            TokenKind.Continue,
+            TokenKind.Eof
+        ]);
+    });
 });
 
 function expectKinds(text: string, tokenKinds: TokenKind[]) {
diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts
index b036885e..c6ea5151 100644
--- a/src/lexer/TokenKind.ts
+++ b/src/lexer/TokenKind.ts
@@ -161,6 +161,7 @@ export enum TokenKind {
     Import = 'Import',
     EndInterface = 'EndInterface',
     Const = 'Const',
+    Continue = 'Continue',
 
     //brighterscript source literals
     LineNumLiteral = 'LineNumLiteral',
@@ -238,6 +239,7 @@ export const ReservedWords = new Set([
 export const Keywords: Record<string, TokenKind> = {
     as: TokenKind.As,
     and: TokenKind.And,
+    continue: TokenKind.Continue,
     dim: TokenKind.Dim,
     end: TokenKind.End,
     then: TokenKind.Then,
@@ -441,7 +443,8 @@ export const AllowedProperties = [
     TokenKind.EndTry,
     TokenKind.Throw,
     TokenKind.EndInterface,
-    TokenKind.Const
+    TokenKind.Const,
+    TokenKind.Continue
 ];
 
 /** List of TokenKind that are allowed as local var identifiers. */
@@ -474,7 +477,8 @@ export const AllowedLocalIdentifiers = [
     TokenKind.Try,
     TokenKind.Catch,
     TokenKind.EndTry,
-    TokenKind.Const
+    TokenKind.Const,
+    TokenKind.Continue
 ];
 
 export const BrighterScriptSourceLiterals = [
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 0d000717..41915686 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -21,6 +21,7 @@ import {
     Block,
     Body,
     CatchStatement,
+    ContinueStatement,
     ClassStatement,
     ConstStatement,
     CommentStatement,
@@ -1109,6 +1110,10 @@ export class Parser {
             return this.gotoStatement();
         }
 
+        if (this.check(TokenKind.Continue)) {
+            return this.continueStatement();
+        }
+
         //does this line look like a label? (i.e.  `someIdentifier:` )
         if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
             try {
@@ -2097,6 +2102,19 @@ export class Parser {
         return new LabelStatement(tokens);
     }
 
+    /**
+     * Parses a `continue` statement
+     */
+    private continueStatement() {
+        return new ContinueStatement({
+            continue: this.advance(),
+            loopType: this.tryConsume(
+                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
+                TokenKind.While, TokenKind.For
+            )
+        });
+    }
+
     /**
      * Parses a `goto` statement
      * @returns an AST representation of an `goto` statement.
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 4133ef2d..7ad82b9a 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -2511,3 +2511,29 @@ export class ConstStatement extends Statement implements TypedefProvider {
         }
     }
 }
+
+export class ContinueStatement extends Statement {
+    constructor(
+        public tokens: {
+            continue: Token;
+            loopType: Token;
+        }
+    ) {
+        super();
+    }
+
+    public get range() {
+        return this.tokens.continue.range;
+    }
+
+    transpile(state: BrsTranspileState) {
+        return [
+            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
+            this.tokens.loopType?.leadingWhitespace ?? ' ',
+            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
+        ];
+    }
+    walk(visitor: WalkVisitor, options: WalkOptions) {
+        //nothing to walk
+    }
+}


Test patch:
diff --git a/src/parser/tests/statement/Continue.spec.ts b/src/parser/tests/statement/Continue.spec.ts
new file mode 100644
index 00000000..f75cb569
--- /dev/null
+++ b/src/parser/tests/statement/Continue.spec.ts
@@ -0,0 +1,98 @@
+import { expect } from 'chai';
+import { createSandbox } from 'sinon';
+import { isContinueStatement } from '../../../astUtils/reflection';
+import { DiagnosticMessages } from '../../../DiagnosticMessages';
+import { TokenKind } from '../../../lexer/TokenKind';
+import { Program } from '../../../Program';
+import { expectDiagnostics, expectZeroDiagnostics, getTestTranspile } from '../../../testHelpers.spec';
+import { standardizePath as s } from '../../../util';
+import type { BrsFile } from '../../../files/BrsFile';
+const sinon = createSandbox();
+
+describe('parser continue statements', () => {
+    let rootDir = s`${process.cwd()}/.tmp/rootDir`;
+    let program: Program;
+    let testTranspile = getTestTranspile(() => [program, rootDir]);
+
+    beforeEach(() => {
+        program = new Program({ rootDir: rootDir, sourceMap: true });
+    });
+    afterEach(() => {
+        sinon.restore();
+        program.dispose();
+    });
+
+    it('parses standalone statement properly', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub main()
+                for i = 0 to 10
+                    continue for
+                end for
+            end sub
+        `);
+        expectZeroDiagnostics(program);
+        expect(file.ast.findChild(isContinueStatement)).to.exist;
+    });
+
+    it('flags incorrect loop type', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub main()
+                for i = 0 to 10
+                    continue while
+                end for
+                for each item in [1, 2, 3]
+                    continue while
+                end for
+                while true
+                    continue for
+                end while
+            end sub
+        `);
+        program.validate();
+        expectDiagnostics(program, [
+            DiagnosticMessages.expectedToken(TokenKind.For),
+            DiagnosticMessages.expectedToken(TokenKind.For),
+            DiagnosticMessages.expectedToken(TokenKind.While)
+        ]);
+        expect(file.ast.findChild(isContinueStatement)).to.exist;
+    });
+
+    it('flags missing `for` or `while` but still creates the node', () => {
+        const file = program.setFile<BrsFile>('source/main.bs', `
+            sub main()
+                for i = 0 to 10
+                    continue
+                end for
+            end sub
+        `);
+        expectDiagnostics(program, [
+            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For)
+        ]);
+        expect(file.ast.findChild(isContinueStatement)).to.exist;
+    });
+
+    it('detects `continue` used outside of a loop', () => {
+        program.setFile<BrsFile>('source/main.bs', `
+            sub main()
+                continue for
+            end sub
+        `);
+        program.validate();
+        expectDiagnostics(program, [
+            DiagnosticMessages.illegalContinueStatement().message
+        ]);
+    });
+
+    it('transpiles properly', () => {
+        testTranspile(`
+            sub main()
+                while true
+                    continue while
+                end while
+                for i = 0 to 10
+                    continue for
+                end for
+            end sub
+        `);
+    });
+});