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

feat(lex,parse): Add stub try/catch implementation #34

Merged
merged 4 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -1035,6 +1035,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
4 changes: 4 additions & 0 deletions src/lexer/Lexeme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export enum Lexeme {
// canonical source: https://sdkdocs.roku.com/display/sdkdoc/Reserved+Words
And = "And",
Box = "Box",
Catch = "Catch",
CreateObject = "CreateObject",
Dim = "Dim",
Else = "Else",
Expand All @@ -77,6 +78,7 @@ export enum Lexeme {
EndFor = "EndFor",
EndIf = "EndIf",
EndSub = "EndSub",
EndTry = "EndTry",
EndWhile = "EndWhile",
Eval = "Eval",
Exit = "Exit",
Expand Down Expand Up @@ -105,8 +107,10 @@ export enum Lexeme {
Stop = "Stop",
Sub = "Sub",
Tab = "Tab",
Throw = "Throw",
To = "To",
True = "True",
Try = "Try",
Type = "Type",
While = "While",

Expand Down
10 changes: 8 additions & 2 deletions src/lexer/ReservedWords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Lexeme as L } from "./Lexeme";

/**
* The set of all reserved words in the reference BrightScript runtime. These can't be used for any
* other purpose within a BrightScript file.
* other purpose (e.g. as identifiers) within a BrightScript file.
* @see https://sdkdocs.roku.com/display/sdkdoc/Reserved+Words
*/
export const ReservedWords = new Set([
Expand Down Expand Up @@ -44,19 +44,21 @@ export const ReservedWords = new Set([
"tab",
"then",
"to",
"throw",
"true",
"type",
"while",
]);

/**
* The set of keywords in the reference BrightScript runtime. Any of these that *are not* reserved
* words can be used within a BrightScript file for other purposes, e.g. `tab`.
* words can be used within a BrightScript file for other purposes as identifiers, e.g. `tab`.
*
* Unfortunately there's no canonical source for this!
*/
export const KeyWords: { [key: string]: L } = {
and: L.And,
catch: L.Catch,
dim: L.Dim,
else: L.Else,
elseif: L.ElseIf,
Expand All @@ -69,6 +71,8 @@ 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,
exit: L.Exit,
Expand All @@ -94,6 +98,8 @@ 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,
};
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
28 changes: 28 additions & 0 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,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 @@ -575,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,
};
}
}
11 changes: 11 additions & 0 deletions test/e2e/Syntax.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,15 @@ describe("end to end syntax", () => {
"optional chaining works",
]);
});
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",
]);
});
});
13 changes: 13 additions & 0 deletions test/e2e/resources/try-catch.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
sub main()
a = 5

print "[pre_try] a = " a
try
a = a * 2
print "[in_try] a = " a
catch e
' currently unimplemented
end try

print "[post_try] a = " a
end sub
22 changes: 19 additions & 3 deletions test/lexer/Lexer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ describe("lexer", () => {
describe("identifiers", () => {
it("matches single-word keywords", () => {
// test just a sample of single-word reserved words for now.
// if we find any that we've missed
let { tokens } = Lexer.scan("and or if else endif return true false line_num");
// if we find any that we've missed, add them here
let { tokens } = Lexer.scan("and or if else endif return true false line_num throw");
expect(tokens.map((w) => w.kind)).toEqual([
Lexeme.And,
Lexeme.Or,
Expand All @@ -403,20 +403,24 @@ describe("lexer", () => {
Lexeme.True,
Lexeme.False,
Lexeme.Identifier,
Lexeme.Throw,
Lexeme.Eof,
]);
expect(tokens.filter((w) => !!w.literal).length).toBe(0);
});

it("matches multi-word keywords", () => {
let { tokens } = Lexer.scan("else if end if end while End Sub end Function Exit wHILe");
let { tokens } = Lexer.scan(
"else if end if end while End Sub end Function Exit wHILe end try"
);
expect(tokens.map((w) => w.kind)).toEqual([
Lexeme.ElseIf,
Lexeme.EndIf,
Lexeme.EndWhile,
Lexeme.EndSub,
Lexeme.EndFunction,
Lexeme.ExitWhile,
Lexeme.EndTry,
Lexeme.Eof,
]);
expect(tokens.filter((w) => !!w.literal).length).toBe(0);
Expand All @@ -431,6 +435,18 @@ describe("lexer", () => {
]);
});

it("reads try/catch/throw properly", () => {
let { tokens } = Lexer.scan("try catch throw end try endtry");
expect(tokens.map((w) => w.kind)).toEqual([
Lexeme.Try,
Lexeme.Catch,
Lexeme.Throw,
Lexeme.EndTry,
Lexeme.EndTry,
Lexeme.Eof,
]);
});

it("matches keywords with silly capitalization", () => {
let { tokens } = Lexer.scan("iF ELSE eNDIf FUncTioN");
expect(tokens.map((w) => w.kind)).toEqual([
Expand Down
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not gonna hold up the PR for this, but you could consider using undent which is a project I wrote to do exactly this. it also supports tagged template literals so you can use it like:

console.log(undent`
    something
         somethingElse
      someThirdLine
`);

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");
};