Skip to content

Commit

Permalink
feat(parse): Consume try, catch, and end try lexemes
Browse files Browse the repository at this point in the history
Throwing exceptions and catching them aren't yet supported in `brs`, so
executing only the `try` block seems to be a reasonable "bare minimum"
approach to try/catch support.  Handle `try`, `catch`, and `end try`
lexemes in the parser, emitting TryCatch statements for the interpreter
to execute naïvely.

see #554
  • Loading branch information
sjbarag committed Jan 19, 2021
1 parent cbe5229 commit fe9077e
Show file tree
Hide file tree
Showing 9 changed files with 1,243 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/coverage/FileCoverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ export class FileCoverage implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType
return BrsInvalid.Instance;
}

visitTryCatch(statement: Stmt.TryCatch) {
// TODO: implement statement/expression coverage for try/catch
return BrsInvalid.Instance;
}

visitFor(statement: Stmt.For) {
this.execute(statement.counterDeclaration);
this.evaluate(statement.counterDeclaration.value);
Expand Down
5 changes: 5 additions & 0 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,11 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
}
}

visitTryCatch(statement: Stmt.TryCatch): BrsInvalid {
this.visitBlock(statement.tryBlock);
return BrsInvalid.Instance;
}

visitBlock(block: Stmt.Block): BrsType {
block.statements.forEach((statement) => this.execute(statement));
return BrsInvalid.Instance;
Expand Down
3 changes: 3 additions & 0 deletions src/lexer/ReservedWords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ReservedWords = new Set([
*/
export const KeyWords: { [key: string]: L } = {
and: L.And,
catch: L.Catch,
dim: L.Dim,
else: L.Else,
elseif: L.ElseIf,
Expand All @@ -70,6 +71,7 @@ export const KeyWords: { [key: string]: L } = {
"end if": L.EndIf,
endsub: L.EndSub,
"end sub": L.EndSub,
endtry: L.EndTry,
"end try": L.EndTry, // note: 'endtry' (no space) is *not* a keyword
endwhile: L.EndWhile,
"end while": L.EndWhile,
Expand All @@ -96,6 +98,7 @@ export const KeyWords: { [key: string]: L } = {
stop: L.Stop,
sub: L.Sub,
to: L.To,
try: L.Try,
throw: L.Throw,
true: L.True,
while: L.While,
Expand Down
42 changes: 40 additions & 2 deletions src/parser/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ type BlockTerminator =
| Lexeme.EndSub
| Lexeme.EndFunction
| Lexeme.Newline // possible only in a single-line `if` statement
| Lexeme.Eof; // possible only in a single-line `if` statement
| Lexeme.Eof // possible only in a single-line `if` statement
| Lexeme.Catch
| Lexeme.EndTry;

/** The set of operators valid for use in assignment statements. */
const assignmentOperators = [
Expand Down Expand Up @@ -90,7 +92,13 @@ const allowedProperties = [
];

/** List of Lexeme that are allowed as local var identifiers. */
const allowedIdentifiers = [Lexeme.EndFor, Lexeme.ExitFor, Lexeme.ForEach];
const allowedIdentifiers = [
Lexeme.EndFor,
Lexeme.ExitFor,
Lexeme.ForEach,
Lexeme.Try,
Lexeme.Catch,
];

/**
* List of string versions of Lexeme that are NOT allowed as local var identifiers.
Expand Down Expand Up @@ -576,6 +584,10 @@ export class Parser {
return stopStatement();
}

if (check(Lexeme.Try)) {
return tryCatch();
}

if (check(Lexeme.If)) {
return ifStatement();
}
Expand Down Expand Up @@ -629,6 +641,32 @@ export class Parser {
return setStatement(...additionalterminators);
}

function tryCatch(): Stmt.TryCatch {
let tryKeyword = advance();
let tryBlock = block(Lexeme.Catch);
if (!tryBlock) {
throw addError(peek(), "Expected 'catch' to terminate try block");
}

if (!check(Lexeme.Identifier)) {
// defer this error so we can parse the `catch` block.
// it'll be thrown if the catch block parses successfully otherwise.
throw addError(peek(), "Expected variable name for caught error after 'catch'");
}

let caughtVariable = new Expr.Variable(advance() as Identifier);
let catchBlock = block(Lexeme.EndTry);
if (!catchBlock) {
throw addError(peek(), "Expected 'end try' or 'endtry' to terminate catch block");
}

return new Stmt.TryCatch(tryBlock.body, catchBlock.body, caughtVariable, {
try: tryKeyword,
catch: tryBlock.closingToken,
endtry: catchBlock.closingToken,
});
}

function whileStatement(): Stmt.While {
const whileKeyword = advance();
const condition = expression();
Expand Down
29 changes: 28 additions & 1 deletion src/parser/Statement.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as Expr from "./Expression";
import { Token, Identifier, Location, Lexeme } from "../lexer";
import { BrsType, BrsInvalid } from "../brsTypes";
import { InvalidZone } from "luxon";
import { AstNode } from "./AstNode";

/** A set of reasons why a `Block` stopped executing. */
Expand All @@ -25,6 +24,7 @@ export interface Visitor<T> {
visitIndexedSet(statement: IndexedSet): BrsType;
visitIncrement(expression: Increment): BrsInvalid;
visitLibrary(statement: Library): BrsInvalid;
visitTryCatch(statement: TryCatch): BrsInvalid;
}

let statementTypes = new Set<string>([
Expand Down Expand Up @@ -576,3 +576,30 @@ export class Library extends AstNode implements Statement {
};
}
}

export class TryCatch extends AstNode implements Statement {
constructor(
readonly tryBlock: Block,
readonly catchBlock: Block,
readonly errorBinding: Expr.Variable,
readonly tokens: {
try: Token;
catch: Token;
endtry: Token;
}
) {
super("TryCatch");
}

accept<R>(visitor: Visitor<R>): BrsType {
return visitor.visitTryCatch(this);
}

get location() {
return {
file: this.tokens.try.location.file,
start: this.tokens.endtry.location.start,
end: this.tokens.endtry.location.end,
};
}
}
12 changes: 12 additions & 0 deletions test/e2e/Syntax.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,16 @@ describe("end to end syntax", () => {
"14", // arr = [13]: arr[0]++
]);
});

test("try-catch.brs", async () => {
await execute([resourceFile("try-catch.brs")], outputStreams);
expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
"[pre_try] a = ",
"5",
"[in_try] a = ",
"10",
"[post_try] a = ",
"10",
]);
});
});
17 changes: 17 additions & 0 deletions test/parser/ParserTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,20 @@ exports.locationEqual = function (loc1, loc2) {
loc1.end.column === loc2.end.column
);
};

/**
* Removes least-common leading indentation from a string, effectively "unindenting" a multi-line
* template string.
* @param {string} str - the string to unindent
* @return {string} `str`, but reformatted so that at least one line starts at column 0
*/
exports.deindent = function deindent(str) {
let lines = str.split("\n");
let firstNonEmptyLine = lines.find((line) => line.trim() !== "");
if (firstNonEmptyLine == null) {
return str;
}

let baseIndent = firstNonEmptyLine.length - firstNonEmptyLine.trim().length;
return lines.map((line) => line.substring(baseIndent)).join("\n");
};
153 changes: 153 additions & 0 deletions test/parser/controlFlow/TryCatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const brs = require("brs");

const { deindent } = require("../ParserTests");

function scan(str) {
return brs.lexer.Lexer.scan(str).tokens;
}

describe("parser try/catch statements", () => {
let parser;

beforeEach(() => {
parser = new brs.parser.Parser();
});

it("requires catch to end try block", () => {
const { errors } = parser.parse(
scan(
deindent(`
try
print "in try"
end try
`)
)
);

expect(errors).toEqual(
expect.arrayContaining([new Error("Found unexpected token 'end try'")])
);
});

it("requires variable binding for caught error", () => {
const { errors } = parser.parse(
scan(
deindent(`
try
print "in try"
catch
print "in catch"
end try
`)
)
);

expect(errors).toEqual(
expect.arrayContaining([
new Error("Expected variable name for caught error after 'catch'"),
])
);
});

it("requires end try or endtry to end catch block", () => {
const { errors } = parser.parse(
scan(
deindent(`
try
print "in try"
catch e
print "in catch"
end if
`)
)
);

expect(errors).toEqual(
expect.arrayContaining([
new Error(
"(At end of file) Expected 'end try' or 'endtry' to terminate catch block"
),
])
);
});

it("accepts try/catch/end try", () => {
const { statements, errors } = parser.parse(
scan(
deindent(`
try
print "in try"
catch e
print "in catch"
end try
`)
)
);

expect(errors).toEqual([]);
expect(statements).toBeDefined();
expect(statements).not.toBeNull();
expect(statements).toMatchSnapshot();
});

it("accepts try/catch/endtry", () => {
const { statements, errors } = parser.parse(
scan(
deindent(`
sub main()
try
print "in try"
catch e
print "in catch"
endtry
end sub
`)
)
);

expect(errors).toEqual([]);
expect(statements).toBeDefined();
expect(statements).not.toBeNull();
expect(statements).toMatchSnapshot();
});

it("allows try/catch to nest in try", () => {
const { statements, errors } = parser.parse(
scan(
deindent(`
try
print "outer try"
try
print "inner try
catch e
print "in upper catch"
end try
catch e
print "in catch"
endtry
`)
)
);

expect(errors).toEqual([]);
expect(statements).toBeDefined();
expect(statements).not.toBeNull();
expect(statements).toMatchSnapshot();
});

it("allows try and catch as variable names", () => {
const { statements, errors } = parser.parse(
scan(
deindent(`
try = "attempt"
catch = "whoops, dropped it"
`)
)
);

expect(errors).toEqual([]);
expect(statements).toBeDefined();
expect(statements).not.toBeNull();
expect(statements).toMatchSnapshot();
});
});
Loading

0 comments on commit fe9077e

Please sign in to comment.