diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index c547a4d71..8c496560e 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -635,6 +635,11 @@ export let DiagnosticMessages = { message: `Cannot find type with name '${typeName}'`, code: 1123, severity: DiagnosticSeverity.Error + }), + illegalContinueStatement: () => ({ + message: `Continue statement must be contained within a loop statement`, + code: 1124, + severity: DiagnosticSeverity.Error }) }; diff --git a/src/lexer/Lexer.spec.ts b/src/lexer/Lexer.spec.ts index 8572a9518..0b7955c06 100644 --- a/src/lexer/Lexer.spec.ts +++ b/src/lexer/Lexer.spec.ts @@ -1294,4 +1294,13 @@ describe('lexer', () => { ); }); }); + + it('detects "continue" as a keyword', () => { + expect( + Lexer.scan('continue').tokens.map(x => x.kind) + ).to.eql([ + TokenKind.Continue, + TokenKind.Eof + ]); + }); }); diff --git a/src/lexer/TokenKind.ts b/src/lexer/TokenKind.ts index 6c58c416c..16213998b 100644 --- a/src/lexer/TokenKind.ts +++ b/src/lexer/TokenKind.ts @@ -155,6 +155,7 @@ export enum TokenKind { Override = 'Override', Import = 'Import', EndInterface = 'EndInterface', + Continue = 'Continue', //brighterscript source literals LineNumLiteral = 'LineNumLiteral', @@ -232,6 +233,7 @@ export const ReservedWords = new Set([ export const Keywords: Record = { as: TokenKind.As, and: TokenKind.And, + continue: TokenKind.Continue, dim: TokenKind.Dim, end: TokenKind.End, then: TokenKind.Then, @@ -429,7 +431,8 @@ export const AllowedProperties = [ TokenKind.Catch, TokenKind.EndTry, TokenKind.Throw, - TokenKind.EndInterface + TokenKind.EndInterface, + TokenKind.Continue ]; /** List of TokenKind that are allowed as local var identifiers. */ @@ -461,7 +464,8 @@ export const AllowedLocalIdentifiers = [ TokenKind.Import, TokenKind.Try, TokenKind.Catch, - TokenKind.EndTry + TokenKind.EndTry, + TokenKind.Continue ]; export const BrighterScriptSourceLiterals = [ diff --git a/src/parser/BrsTranspileState.ts b/src/parser/BrsTranspileState.ts index d83e015a2..d19a27295 100644 --- a/src/parser/BrsTranspileState.ts +++ b/src/parser/BrsTranspileState.ts @@ -28,4 +28,31 @@ export class BrsTranspileState extends TranspileState { * Used by ClassMethodStatements to determine information about their enclosing class */ public classStatement?: ClassStatement; + + private loopLabels = [] as { label: string; wasAccessed: boolean }[]; + + /** + * Start a new loop label tracker. If any continue statements need this, they will + */ + public pushLoopLabel() { + this.loopLabels.push({ + label: `BRIGHTERSCRIPT_LABEL_${this.loopLabelSequence++}`, + wasAccessed: false + }); + } + + public popLoopLabel() { + return this.loopLabels.pop(); + } + + /** + * Get the name of the label at the end of the loop. By calling this, we tell + * the loop transpiler that it needs to insert the end-of-loop label + */ + public getLoopLabel() { + const loopLabel = this.loopLabels[this.loopLabels.length - 1]; + loopLabel.wasAccessed = true; + return loopLabel.label; + } + private loopLabelSequence = 0; } diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 7af59b49d..503af9c03 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -93,7 +93,7 @@ import { Logger } from '../Logger'; import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isClassMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection'; import { createVisitor, WalkMode } from '../astUtils/visitors'; import { createStringLiteral, createToken } from '../astUtils/creators'; -import { RegexLiteralExpression } from '.'; +import { ContinueStatement, RegexLiteralExpression } from '.'; export class Parser { /** @@ -1004,6 +1004,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 { @@ -1972,6 +1976,15 @@ export class Parser { return new LabelStatement(tokens); } + /** + * Parses a `continue` statement + */ + private continueStatement() { + return new ContinueStatement({ + continue: this.advance() + }); + } + /** * 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 3c121bca4..ce90d48a5 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -805,6 +805,7 @@ export class ForStatement extends Statement { public readonly range: Range; transpile(state: BrsTranspileState) { + state.pushLoopLabel(); let result = []; //for result.push( @@ -836,15 +837,22 @@ export class ForStatement extends Statement { state.lineage.unshift(this); result.push(...this.body.transpile(state)); state.lineage.shift(); + + //if we need to include an end-of-loop label + const loopLabel = state.popLoopLabel(); + if (loopLabel.wasAccessed) { + result.push('\n', state.indent(), loopLabel.label, ':'); + } + if (this.body.statements.length > 0) { result.push('\n'); } + //end for result.push( state.indent(), state.transpileToken(this.endForToken) ); - return result; } @@ -2173,3 +2181,26 @@ export class ThrowStatement extends Statement { } } } + +export class ContinueStatement extends Statement { + constructor( + public tokens: { + continue: Token; + } + ) { + super(); + } + + public get range() { + return this.tokens.continue.range; + } + + transpile(state: BrsTranspileState) { + return [ + state.sourceNode(this.tokens.continue, `goto ${state.getLoopLabel()}`) + ]; + } + walk(visitor: WalkVisitor, options: WalkOptions) { + //nothing to walk + } +} diff --git a/src/parser/tests/statement/Continue.spec.ts b/src/parser/tests/statement/Continue.spec.ts new file mode 100644 index 000000000..785bb67d8 --- /dev/null +++ b/src/parser/tests/statement/Continue.spec.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; +import type { ForStatement, FunctionStatement } from '../..'; +import { ContinueStatement } from '../..'; +import { DiagnosticMessages } from '../../../DiagnosticMessages'; +import { expectZeroDiagnostics } from '../../../testHelpers.spec'; +import { Parser } from '../../Parser'; + +describe.only('parser continue statements', () => { + it('parses standalone statement properly', () => { + let parser = Parser.parse(` + sub main() + for i = 0 to 10 + continue + end for + end sub + `); + expectZeroDiagnostics(parser); + const stmt = ((parser.ast.statements[0] as FunctionStatement).func.body.statements[0] as ForStatement).body.statements[0] as ContinueStatement; + expect(stmt).to.be.instanceof(ContinueStatement); + }); + + it('detects `continue` used outside of a loop', () => { + let parser = Parser.parse(` + sub main() + continue + end sub + `); + expect(parser.diagnostics[0]?.message).to.eql( + DiagnosticMessages.illegalContinueStatement().message + ); + }); +});