Skip to content

Commit fba1c3a

Browse files
Syntax and transpile support for continue statement (#697)
* Add parse and transpile support for continue * fix lint issues
1 parent 77e9f00 commit fba1c3a

9 files changed

Lines changed: 210 additions & 8 deletions

File tree

src/DiagnosticMessages.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,8 @@ export let DiagnosticMessages = {
572572
code: 1108,
573573
severity: DiagnosticSeverity.Error
574574
}),
575-
expectedToken: (tokenKind: string) => ({
576-
message: `Expected '${tokenKind}'`,
575+
expectedToken: (...tokenKinds: string[]) => ({
576+
message: `Expected token '${tokenKinds.join(`' or '`)}'`,
577577
code: 1109,
578578
severity: DiagnosticSeverity.Error
579579
}),
@@ -699,6 +699,11 @@ export let DiagnosticMessages = {
699699
message: `Expected directory depth no larger than 7, but found ${numberOfParentDirectories}.`,
700700
code: 1134,
701701
severity: DiagnosticSeverity.Error
702+
}),
703+
illegalContinueStatement: () => ({
704+
message: `Continue statement must be contained within a loop statement`,
705+
code: 1135,
706+
severity: DiagnosticSeverity.Error
702707
})
703708
};
704709

src/astUtils/reflection.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
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';
1+
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';
22
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';
33
import type { BrsFile } from '../files/BrsFile';
44
import type { XmlFile } from '../files/XmlFile';
@@ -162,6 +162,9 @@ export function isEnumMemberStatement(element: AstNode | undefined): element is
162162
export function isConstStatement(element: AstNode | undefined): element is ConstStatement {
163163
return element?.constructor.name === 'ConstStatement';
164164
}
165+
export function isContinueStatement(element: AstNode | undefined): element is ContinueStatement {
166+
return element?.constructor.name === 'ContinueStatement';
167+
}
165168
export function isTryCatchStatement(element: AstNode | undefined): element is TryCatchStatement {
166169
return element?.constructor.name === 'TryCatchStatement';
167170
}

src/astUtils/visitors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-bitwise */
22
import type { CancellationToken } from 'vscode-languageserver';
3-
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';
3+
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';
44
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';
55
import { isExpression, isStatement } from './reflection';
66
import type { AstEditor } from './AstEditor';
@@ -127,6 +127,7 @@ export function createVisitor(
127127
* @deprecated use `FieldStatement`
128128
*/
129129
ClassFieldStatement?: (statement: ClassFieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
130+
ContinueStatement?: (statement: ContinueStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
130131
MethodStatement?: (statement: MethodStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
131132
FieldStatement?: (statement: FieldStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;
132133
TryCatchStatement?: (statement: TryCatchStatement, parent?: Statement, owner?: any, key?: any) => Statement | void;

src/bscPlugin/validation/BrsFileValidator.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement } from '../../astUtils/reflection';
1+
import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isForEachStatement, isForStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, isNamespaceStatement, isWhileStatement } from '../../astUtils/reflection';
22
import { createVisitor, WalkMode } from '../../astUtils/visitors';
33
import { DiagnosticMessages } from '../../DiagnosticMessages';
44
import type { BrsFile } from '../../files/BrsFile';
@@ -7,7 +7,7 @@ import { TokenKind } from '../../lexer/TokenKind';
77
import type { AstNode } from '../../parser/AstNode';
88
import type { LiteralExpression } from '../../parser/Expression';
99
import { ParseMode } from '../../parser/Parser';
10-
import type { EnumMemberStatement, EnumStatement, ImportStatement, LibraryStatement } from '../../parser/Statement';
10+
import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, WhileStatement } from '../../parser/Statement';
1111
import { DynamicType } from '../../types/DynamicType';
1212
import util from '../../util';
1313

@@ -146,6 +146,9 @@ export class BrsFileValidator {
146146
if (node.identifier) {
147147
node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
148148
}
149+
},
150+
ContinueStatement: (node) => {
151+
this.validateContinueStatement(node);
149152
}
150153
});
151154

@@ -299,4 +302,39 @@ export class BrsFileValidator {
299302
}
300303
}
301304
}
305+
306+
private validateContinueStatement(statement: ContinueStatement) {
307+
const validateLoopTypeMatch = (loopType: TokenKind) => {
308+
//coerce ForEach to For
309+
loopType = loopType === TokenKind.ForEach ? TokenKind.For : loopType;
310+
311+
if (loopType?.toLowerCase() !== statement.tokens.loopType.text?.toLowerCase()) {
312+
this.event.file.addDiagnostic({
313+
range: statement.tokens.loopType.range,
314+
...DiagnosticMessages.expectedToken(loopType)
315+
});
316+
}
317+
};
318+
319+
//find the parent loop statement
320+
const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
321+
if (isWhileStatement(node)) {
322+
validateLoopTypeMatch(node.tokens.while.kind);
323+
return true;
324+
} else if (isForStatement(node)) {
325+
validateLoopTypeMatch(node.forToken.kind);
326+
return true;
327+
} else if (isForEachStatement(node)) {
328+
validateLoopTypeMatch(node.tokens.forEach.kind);
329+
return true;
330+
}
331+
});
332+
//flag continue statements found outside of a loop
333+
if (!parent) {
334+
this.event.file.addDiagnostic({
335+
range: statement.range,
336+
...DiagnosticMessages.illegalContinueStatement()
337+
});
338+
}
339+
}
302340
}

src/lexer/Lexer.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,15 @@ describe('lexer', () => {
13681368
);
13691369
});
13701370
});
1371+
1372+
it('detects "continue" as a keyword', () => {
1373+
expect(
1374+
Lexer.scan('continue').tokens.map(x => x.kind)
1375+
).to.eql([
1376+
TokenKind.Continue,
1377+
TokenKind.Eof
1378+
]);
1379+
});
13711380
});
13721381

13731382
function expectKinds(text: string, tokenKinds: TokenKind[]) {

src/lexer/TokenKind.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export enum TokenKind {
161161
Import = 'Import',
162162
EndInterface = 'EndInterface',
163163
Const = 'Const',
164+
Continue = 'Continue',
164165

165166
//brighterscript source literals
166167
LineNumLiteral = 'LineNumLiteral',
@@ -238,6 +239,7 @@ export const ReservedWords = new Set([
238239
export const Keywords: Record<string, TokenKind> = {
239240
as: TokenKind.As,
240241
and: TokenKind.And,
242+
continue: TokenKind.Continue,
241243
dim: TokenKind.Dim,
242244
end: TokenKind.End,
243245
then: TokenKind.Then,
@@ -441,7 +443,8 @@ export const AllowedProperties = [
441443
TokenKind.EndTry,
442444
TokenKind.Throw,
443445
TokenKind.EndInterface,
444-
TokenKind.Const
446+
TokenKind.Const,
447+
TokenKind.Continue
445448
];
446449

447450
/** List of TokenKind that are allowed as local var identifiers. */
@@ -474,7 +477,8 @@ export const AllowedLocalIdentifiers = [
474477
TokenKind.Try,
475478
TokenKind.Catch,
476479
TokenKind.EndTry,
477-
TokenKind.Const
480+
TokenKind.Const,
481+
TokenKind.Continue
478482
];
479483

480484
export const BrighterScriptSourceLiterals = [

src/parser/Parser.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
Block,
2222
Body,
2323
CatchStatement,
24+
ContinueStatement,
2425
ClassStatement,
2526
ConstStatement,
2627
CommentStatement,
@@ -1109,6 +1110,10 @@ export class Parser {
11091110
return this.gotoStatement();
11101111
}
11111112

1113+
if (this.check(TokenKind.Continue)) {
1114+
return this.continueStatement();
1115+
}
1116+
11121117
//does this line look like a label? (i.e. `someIdentifier:` )
11131118
if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
11141119
try {
@@ -2097,6 +2102,19 @@ export class Parser {
20972102
return new LabelStatement(tokens);
20982103
}
20992104

2105+
/**
2106+
* Parses a `continue` statement
2107+
*/
2108+
private continueStatement() {
2109+
return new ContinueStatement({
2110+
continue: this.advance(),
2111+
loopType: this.tryConsume(
2112+
DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2113+
TokenKind.While, TokenKind.For
2114+
)
2115+
});
2116+
}
2117+
21002118
/**
21012119
* Parses a `goto` statement
21022120
* @returns an AST representation of an `goto` statement.

src/parser/Statement.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,3 +2511,29 @@ export class ConstStatement extends Statement implements TypedefProvider {
25112511
}
25122512
}
25132513
}
2514+
2515+
export class ContinueStatement extends Statement {
2516+
constructor(
2517+
public tokens: {
2518+
continue: Token;
2519+
loopType: Token;
2520+
}
2521+
) {
2522+
super();
2523+
}
2524+
2525+
public get range() {
2526+
return this.tokens.continue.range;
2527+
}
2528+
2529+
transpile(state: BrsTranspileState) {
2530+
return [
2531+
state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
2532+
this.tokens.loopType?.leadingWhitespace ?? ' ',
2533+
state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
2534+
];
2535+
}
2536+
walk(visitor: WalkVisitor, options: WalkOptions) {
2537+
//nothing to walk
2538+
}
2539+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect } from 'chai';
2+
import { createSandbox } from 'sinon';
3+
import { isContinueStatement } from '../../../astUtils/reflection';
4+
import { DiagnosticMessages } from '../../../DiagnosticMessages';
5+
import { TokenKind } from '../../../lexer/TokenKind';
6+
import { Program } from '../../../Program';
7+
import { expectDiagnostics, expectZeroDiagnostics, getTestTranspile } from '../../../testHelpers.spec';
8+
import { standardizePath as s } from '../../../util';
9+
import type { BrsFile } from '../../../files/BrsFile';
10+
const sinon = createSandbox();
11+
12+
describe('parser continue statements', () => {
13+
let rootDir = s`${process.cwd()}/.tmp/rootDir`;
14+
let program: Program;
15+
let testTranspile = getTestTranspile(() => [program, rootDir]);
16+
17+
beforeEach(() => {
18+
program = new Program({ rootDir: rootDir, sourceMap: true });
19+
});
20+
afterEach(() => {
21+
sinon.restore();
22+
program.dispose();
23+
});
24+
25+
it('parses standalone statement properly', () => {
26+
const file = program.setFile<BrsFile>('source/main.bs', `
27+
sub main()
28+
for i = 0 to 10
29+
continue for
30+
end for
31+
end sub
32+
`);
33+
expectZeroDiagnostics(program);
34+
expect(file.ast.findChild(isContinueStatement)).to.exist;
35+
});
36+
37+
it('flags incorrect loop type', () => {
38+
const file = program.setFile<BrsFile>('source/main.bs', `
39+
sub main()
40+
for i = 0 to 10
41+
continue while
42+
end for
43+
for each item in [1, 2, 3]
44+
continue while
45+
end for
46+
while true
47+
continue for
48+
end while
49+
end sub
50+
`);
51+
program.validate();
52+
expectDiagnostics(program, [
53+
DiagnosticMessages.expectedToken(TokenKind.For),
54+
DiagnosticMessages.expectedToken(TokenKind.For),
55+
DiagnosticMessages.expectedToken(TokenKind.While)
56+
]);
57+
expect(file.ast.findChild(isContinueStatement)).to.exist;
58+
});
59+
60+
it('flags missing `for` or `while` but still creates the node', () => {
61+
const file = program.setFile<BrsFile>('source/main.bs', `
62+
sub main()
63+
for i = 0 to 10
64+
continue
65+
end for
66+
end sub
67+
`);
68+
expectDiagnostics(program, [
69+
DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For)
70+
]);
71+
expect(file.ast.findChild(isContinueStatement)).to.exist;
72+
});
73+
74+
it('detects `continue` used outside of a loop', () => {
75+
program.setFile<BrsFile>('source/main.bs', `
76+
sub main()
77+
continue for
78+
end sub
79+
`);
80+
program.validate();
81+
expectDiagnostics(program, [
82+
DiagnosticMessages.illegalContinueStatement().message
83+
]);
84+
});
85+
86+
it('transpiles properly', () => {
87+
testTranspile(`
88+
sub main()
89+
while true
90+
continue while
91+
end while
92+
for i = 0 to 10
93+
continue for
94+
end for
95+
end sub
96+
`);
97+
});
98+
});

0 commit comments

Comments
 (0)