Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/continue statement #489

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
};

Expand Down
9 changes: 9 additions & 0 deletions src/lexer/Lexer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
]);
});
});
8 changes: 6 additions & 2 deletions src/lexer/TokenKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export enum TokenKind {
Override = 'Override',
Import = 'Import',
EndInterface = 'EndInterface',
Continue = 'Continue',

//brighterscript source literals
LineNumLiteral = 'LineNumLiteral',
Expand Down Expand Up @@ -232,6 +233,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,
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -461,7 +464,8 @@ export const AllowedLocalIdentifiers = [
TokenKind.Import,
TokenKind.Try,
TokenKind.Catch,
TokenKind.EndTry
TokenKind.EndTry,
TokenKind.Continue
];

export const BrighterScriptSourceLiterals = [
Expand Down
27 changes: 27 additions & 0 deletions src/parser/BrsTranspileState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
15 changes: 14 additions & 1 deletion src/parser/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
33 changes: 32 additions & 1 deletion src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ export class ForStatement extends Statement {
public readonly range: Range;

transpile(state: BrsTranspileState) {
state.pushLoopLabel();
let result = [];
//for
result.push(
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
}
}
32 changes: 32 additions & 0 deletions src/parser/tests/statement/Continue.spec.ts
Original file line number Diff line number Diff line change
@@ -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
);
});
});